Friday, April 29, 2016

Friday Fun XXVIII

And another friday which means time for some fun again...
It's interesting but again I've found a control that looks simple when you see it for the first time but at a second look you will figure out that it's not so easy to implement.
So here it is...



Apart from that the white bar seems to be misaligned in the picture above, the control has a neat effect. First of all the semitransparent white ring in the background makes it possible to put that control on whatever color and because it is translucent the background color will shine through like on the picture above.
In addition it comes with some 3D effect that is produced by the drop shadow of this ring.
Exactly this drop shadow is the thing that looks easy to do but it's not and the reason is the following.
If you create a ring (e.g. using a JavaFX Arc object) you can give it a strokeLineWidth and a Color. Adding a DropShadow is a no-brainer in JavaFX and in principle that's all you have to do. But because the Arc is translucent we will see the DropShadow shine through which will make the Arc appear darker which exactly is the problem.
To give you an idea what I'm talking about, here is a little screenshot of the effect...



As you can see we have a nice DropShadow on the Arc but we also see it shine through the ring which makes it appear dark. If you take a look on the picture above you will see that the ring appears bright and translucent plus the DropShadow...so how to achieve that???
Well the answer is trivial but the solution not... :)
First we need to draw an Arc with a DropShadow that will look as follows...



In the next step we have to create a mask that only contains the shadow without the ring, which we can do by creating two rings. Then we subtract the smaller ring from the bigger one like follows...



On the right side we now have our shape that we can use as a clipping mask which will only contain the DropShadow without the orange ring.
Now we can apply this clipping mask to the Arc that has the DropShadow and it will look similar to this (the white should be transparent)...



And in the last step we can draw the final Arc that will be filled with Color.rgb(255, 255, 255, 0.4) so that the background can shine through.
The code to realize this operation will look as follows...


Arc outerRing = new Arc(size * 0.5, size * 0.5,
                         size * 0.43125, size * 0.43125,
                         0, 360);
outerRing.setFill(null);
outerRing.setStroke(Color.WHITE);
outerRing.setStrokeLineCap(StrokeLineCap.BUTT);
outerRing.setStrokeWidth(size * 0.3);

Arc innerRing = new Arc(size * 0.5, size * 0.5,
                        size * 0.43125, size * 0.43125,
                        0, 360);
innerRing.setFill(null);
innerRing.setStroke(Color.WHITE);
innerRing.setStrokeLineCap(StrokeLineCap.BUTT);
innerRing.setStrokeWidth(size * 0.1375);

Shape shape = Shape.subtract(outerRing, innerRing);

backgroundRing.setCenterX(center);
backgroundRing.setCenterY(center);
backgroundRing.setRadiusX(size * 0.43125);
backgroundRing.setRadiusY(size * 0.43125);
backgroundRing.setStrokeWidth(size * 0.1375);
backgroundRing.setClip(shape);

barBackground.setCenterX(center);
barBackground.setCenterY(center);
barBackground.setRadiusX(size * 0.43125);
barBackground.setRadiusY(size * 0.43125);
barBackground.setStrokeWidth(size * 0.1375);


And we have to do this clipping everytime the size of the control changed. The final result of this operations will then look like follows...



And this is exactly how it should look like :)
For the implementation I make use of the Medusa gauge so there is a dependency on the Medusa project.

If you would like to see the code you will find it on github as always.

That's it for today, so I wish you all a nice weekend and...keep coding...

Friday, April 15, 2016

Friday Fun XXVII

And another friday and again time for some fun...
Well this time the control is small...very small and doesn't have any special features but I needed it and so I thought it might be a good idea to create it.
If you own an Android phone you might know the Material Design Circular Progress Indicator (indeterminate) which looks like this...



Like last weeks friday fun component the problem with this control is not obvious. In principle it's a simple arc where the arcs angle extend accelerates until it gets near to the arcs start, then it decelerates and the arc start angle accelerates. This happens in a combination with a rotation of the whole arc which gives us the nice effect.
I've tried different approaches before I came to the conclusion that an animated dashed arc might be the solution.
To be honest it was a lot of trial and error before I got the parameters right (at least good enough for this control).
After I've found a set of parameters that worked out I realized that the whole thing is dependend on the current size of the control. Means if you increase the size of the circle the dash array needs to be updated otherwise you will suddenly see more than one line moving around. Because it tooks some time to figure out the right parameters for one size I've decided to go with the easy approach...simply scaling the circle instead of really resizing it.
To make sure the circle always stays in the center of the pane I've exchanged the Pane (that I usually use) with a StackPane which does the centering for me. With this approach I'm able to create the circle and resize it properly. The progress indicator will switch to the indeterminate state if the progress is < 0. You can simply call .setProgress(ProgressIndicator.INDETERMINATE) to achieve that. If the progress is between 0 - 1 the circle will be replaced by an arc that shows the current progress. I did not add animation to the standard progress (0 - 1) which could be done by yourself if you like...simply animate the progress between 0 and 1 :)
So here is the result and because it doesn't make sense to show you a screen shot I've recorded a little video...




Another tiny blogpost finished...now enjoy your weekend and if you are interested in the code you can find it on github as always...

sources on github

Oh and don't forget...keep coding... :)

Friday, April 8, 2016

Friday Fun XXVI

And Friday again...finally :)
Today I have another little fun JavaFX component for you which took me some time to build. Those of you that own an Apple Watch (unfortunately I don't have one) might know this control already.
It is the Fitness Gauge that looks like this...



Well in principle this is an easy to do control...BUT...if you take close look the engineers/designers at Apple did again a fantastic job by creating this control. First of all it has a conical gradient on each arc (from a darker to a brighter color). Then it has this neat drop shadow at the end of the arc when it overlaps. 
This seems to be easy to implement but it's not...believe me :)
After some tweaking I got a solution that I'm quite happy with and this is what it looks like...



As you can see it's not really the same but it's not bad :)
You can set each bar individually, the max value for each bar and the color. The control is based on my Medusa library and uses three Medusa Gauges for the data model.

Here is also a little video that gives you an impression how it looks like in action...




So I think that's it for today...just a little fun...enjoy and btw if you need the code...here you go...

Github repository

Keep coding...

Thursday, March 10, 2016

Friday Fun XXV

And Friday again...means time for some fun... :)
This time I have two three (I've recently added a ColorRegulator) controls for you. When I was working on my mood light I stumbled upon some nice images on the web and one of those image was the following that I've found on Dribbble...


The temperature control on the right side of the image looked nice to me and I thought it might be worth creating a JavaFX control of it.
The control above only acts as some kind of slider which shows the target value but I thought by myself it would be nice it would also show the current value. Because then one can use it also as a display of the current temperature. So here is what I came up with...


As you can see it's really similar to the original image except some additional features...
  • Target value will be shown on top
  • Icon to visualize the property
  • An overlay on the color bar that shows the current value
The target value and the overlay will disappear as soon as the (int) current value == (in) target value.
When I've started this control I only thought about a temperature control but you can use it for whatever you like. Therefore I've added the possibility to set a custom color gradient for the bar and you could also define your own icon. 
The control will fire three different events
  • RegulatorEvent.TARGET_SET  fired when target value was set (Mouse released)
  • RegulatorEvent.ADJUSTING    in case the (int) current value != (int) target value
  • RegulatorEvent.ADJUSTED      in case the (int) current value == (int) target value
With this you could also use the control to define a point on the scale by using the rotation ring and as soon as the current value will reach the selected value it will fire an event on which you can react...you could think about a battery charger control that informs you if the battery is charged to 80%.
If we take the charger example let's have a look how to configure the control in this case...
FeedbackRegulator feedbackRegulator = 
    FeedbackRegulatorBuilder.create()
                            .prefSize(400, 400)
                            .minValue(0)
                            .maxValue(100)
                            .targetValue(80)
                            .currentValue(25)
                            .unit("%")
                            .gradientStops(new Stop(0.0, Color.RED),
                                     new Stop(0.5, Color.YELLOW),
                                     new Stop(0.75, Color.GREEN),
                                     new Stop(1.0, Color.LIME))
                            .symbolPath(1, 0.71428571, "M 11.7829 11.7647 L 9.3333 20 L 17.5 8.2353 L 12.7171 " +
                                                       "8.2353 L 15.1667 0 L 7 11.7647 L 11.7829 11.7647 ZM 1.1667 " +
                                                       "17.6471 L 8.8138 17.6471 L 9.5156 15.2941 L 2.3333 15.2941 " +
                                                       "L 2.3333 4.7059 L 10.4749 4.7059 L 12.1087 2.3529 L 1.1667 " +
                                                       "2.3529 C 0.5218 2.3529 0 2.8791 0 3.5294 L 0 16.4706 C 0 " +
                                                       "17.1209 0.5218 17.6471 1.1667 17.6471 ZM 26.8333 5.8824 L " +
                                                       "24.5 5.8824 L 24.5 3.5294 C 24.5 2.8791 23.9782 2.3529 23.3333" +
                                                       " 2.3529 L 15.6839 2.3529 L 14.9844 4.7059 L 22.1667 4.7059 " +
                                                       "L 22.1667 15.2941 L 14.0228 15.2941 L 12.3913 17.6471 " +
                                                       "L 23.3333 17.6471 C 23.9782 17.6471 24.5 17.1209 24.5 16.4706 " +
                                                       "L 24.5 14.1176 L 26.8333 14.1176 C 27.4782 14.1176 28 13.5915 " +
                                                       "28 12.9412 L 28 7.0588 C 28 6.4085 27.4782 5.8824 26.8333 5.8824 Z")
                            .build();

As you can see we can define the min- and maxValue of the control, the unit and the stops for the gradient bar. In addition we can define the symbolPath which is a SVG path and to get the scaling right we also have to define the scaleX and scaleY values for this path.
To get this path you usually use a vector drawing program like Inkscape, Adobe Illustrator etc. and export the vector drawing of the icon as SVG file. Usually you will find a <path> tag in the SVG file that should look similar to the above string. Simply copy that string that normally starts with a M for MoveTo and ends with a Z for ClosePath. To get the scaling right you simply have to calculate the scaling factor from the size of the image, e.g. the charging battery icon that I used here has the following size...

width : 28px
height: 20px

Divide the smaller value by the bigger value and set the bigger value to 1.0. This will give us the following values...

scaleY: 20 / 28 = 0.71428571
scaleX: 1.0

That's all you need to set your customized icon in the control and the result of the code above will look like this...


In this case we could also hook up an EventHandler to the FeedbackEvent.ADJUSTED EventType that will be fired as soon as the battery will be charged by 80%. To realize that you can simply add one line to the builder like follows...
.onAdjusted(e -> System.out.println("Battery charge is 80%"))
But you could also attach the handler later on in your code like follows
feedbackRegulator.addEventHandler(RegulatorEvent.ADJUSTED, event -> System.out.println("Battery charge is 80%"));
So far so good but now that I had a control with a ring to set values I thought it might also be useful to have a similar control that really only acts as some kind of a slider.
Long story short...here it is...


As you can see it's very similar to the other control except it can't visualize a current value but only the target value. So you can use it to control for example the brightness of a light etc.
In principle it comes with the same features as the FeedbackRegulator control except the gradient stops and the current value. But it has an additional property which is the barColor. So to create a control with the custom icon you can simply use the same approach as for the FeedbackRegulator. Here is a little example with a different bar color and a unit...
Regulator regulator = RegulatorBuilder.create()
                                      .prefSize(400, 400)
                                      .barColor(Color.rgb(255, 222, 102))
                                      .unit("%")
                                      .build();
And the result will look like this...


I did not implement properties for different colors of the ring, background etc. but if you are interested in changing it I (in the meantime I've added features to change the control color, text color and symbol color.)...fork it on github :)

Here is a screenshot of the currently available regulator controls...


As you can see on the right I've also added another color selector :)

That's it for today...so keep coding...

Friday, March 4, 2016

Friday Fun XXIV

Last weekend I was playing around with my self-made mood light based on an ESP8266 in combination with MQTT and JavaFX on mobile using Gluon Mobile.
In my first version I used 3 sliders to select the color for the mood light. Well that worked BUT it looked aweful. And because of that I was thinking about a way to choose a color that makes more sense and came up with a drawing that looks like this...


One can select a color by clicking/touching on the color ring and the center circle will show the selected color. You could also drag the mouse/move the finger over the ring and the inner circle will change it's color. The selected color will only be set when the mouse/finger is released. I've implemented it like this because I need to send the selected color to the ESP8266 and I want to avoid sending too many messages.
So I've added a listener to the selectedColorProperty() of the control and send a message to my ESP8266 when the selected color changes.
Clicking the ring will directly change the color.
The tricky parts when creating this control are the conical gradient and the ring itself. For the conical gradient I've created a solution some time ago which I used here.
The ring is a bit special, you could either fake a ring by using a big circle and draw another circle on top of it and fill it with the background color. This is the easiest solution which will definitely work and is easy to scale.
But because I would like to create a real ring and have the space between the inner circle and the ring transparent I needed a different approach.
When drawing in vector programs you simply subtract a small circle from a big circle and you get a ring. This ring can be exported as SVG and you can use it in your CSS file (-fx-scale-shape: true; and -fx-shape: "SVG PATH STRING" are your friends here). This solution works and is also scalable by setting the preferredSize of the CSS-styled Region.
But there is another way of realizing it...you can use
double center = PREFERRED_WIDTH * 0.5;
Shape  ring   = Shape.subtract(new Circle(center, center, center), 
                               new Circle(center, center, PREFERRED_WIDTH * 0.25));
This solution will give you a shape that is a ring...sounds perfect right? Well it comes with a little drawback, the scalability is not that easy as with the other solutions. Because a JavaFX Shape doesn't have parameters like preferredSize, witdh and height, the only way you can scale it is using a Transform.
That's in principle not a big deal but because I use my own ConicalGradient to fill it, it was a bit tricky to figure out how to scale it correctly.
Usually I use something similar like the following code to place a node on the scene graph...

ring.setTranslateX((size - ring.getLayoutBounds().getWidth()) * 0.5);
ring.setTranslateY((size - ring.getLayoutBounds().getHeight()) * 0.5);
But in this case that won't work because the Scale transformation doesn't affect the layout bounds of the shape. Means we always get back the same layout bounds...no matter how the scaling is.
The solution here is to define a pivot point for the Scale transformation to place it correctly.
So the code to scale and place the ring in the right way looks like this...
double scaleFactor = size / PREFERRED_WIDTH;
ring.getTransforms().setAll(new Scale(scaleFactor, scaleFactor, 0, 0));
With this code the ring will always be scaled and located in the center of the control.

The plain control will look like follows...



So it uses the following colors for visualization...

  • Red               Color.rgb(255, 0, 0)
  • Orange          Color.rgb(255, 127, 0)
  • Yellow           Color.rgb(255, 255, 0)
  • Yellow-Green Color.rgb(127, 255, 0)
  • Green            Color.rgb(0, 255, 0)
  • Cyan              Color.rgb(0, 255, 255)
  • Blue               Color.rgb(0, 0, 255)
  • Magenta         Color.rgb(255, 0, 255)
  • Red                Color.rgb(255, 0, 0)

But the ColorSelector control also has a constructor that takes a list of Stop Objects so that you can use the colors of your choice. So here are some examples...
Stop[] stops = { new Stop(0.0, Color.rgb(255,255,0)),
                 new Stop(0.125, Color.rgb(255,0,0)),
                 new Stop(0.375, Color.rgb(255,0,255)),
                 new Stop(0.5, Color.rgb(0,0,255)),
                 new Stop(0.625, Color.rgb(0,255,255)),
                 new Stop(0.875, Color.rgb(0,255,0)),
                 new Stop(1.0, Color.rgb(255,255,0))};

ColorSelector colorSelector = new ColorSelector(stops);
colorSelector.setPrefSize(400, 400);
Which will look like this...


or a monochromatic version...

Stop[] stops = { new Stop(0.0, Color.rgb(63, 63, 242)),
                 new Stop(0.125, Color.rgb(48, 92, 246)),
                 new Stop(0.25, Color.rgb(32, 120, 249)),
                 new Stop(0.375, Color.rgb(16, 149, 252)),
                 new Stop(0.5, Color.rgb(0, 177, 255)),
                 new Stop(0.625, Color.rgb(16, 149, 252)),
                 new Stop(0.75, Color.rgb(32, 120, 249)),
                 new Stop(0.875, Color.rgb(48, 92, 246)),
                 new Stop(1.0, Color.rgb(63, 63, 242))};

ColorSelector colorSelector = new ColorSelector(stops);
which gives you the following look...


Keep in mind that when using the ConicalGradient you should make sure that the first and the last Color are always the same.

I'm not sure if this control is useful for you but I like it...

The code can be found as always on github...

keep coding... :)

Friday, February 26, 2016

FridayFun XXIII

Finally Friday again...
I just recognized that it's more than a year ago that I posted a Friday Fun Component...so here you go...

Last Monday I was skimming the web for interesting controls and stumbled upon the following image.



This might be a very handy gauge for some visualizations like health dashboards etc. In principle it would be very easy to realize something like this because it just contains a few visual parts.
The interesting part of that control is the wavy shaped top edge of the inner fill. If you would like to keep it static this is also no big deal BUT...would it not be cooler if the surface will be animated???
I found some code on the web that did something similar and ported it to Java.

Long story short, here is a little video of the control in action...



I've added the code to the medusa demo project that you can find on github (it is called FunLevelGauge).
Instead of using a special Skin for the Medusa Gauge I've simply took a Region and added a Medusa Gauge as member variable.
The code for the Gauge is very simple and looks like this...

Gauge gauge = GaugeBuilder.create()
                          .minValue(0)
                          .maxValue(1)
                          .animated(true)
                          .build();
As you can see this is really nothing special. For the visualization I used a JavaFX Canvas node that I clipped with a simple JavaFX Circle element.
The ring around the control is again a JavaFX Circle that has a transparent fill and a stroke with the same color as the inner part.
The JavaFX Text node in the center of the control changes it's color dependent on the fill level of the control. The color for the text will be derived from the fill color.
If you would like to play around with the waviness of the surface you might want to play around with the following parameters...
  • detail        (no of points used for the wave)
  • friction            
  • density 
In the example I add an impulse to the wave every second to keep the wave moving but you could also think about to add an impulse only when a new level was set like this...
public void setLevel(final double LEVEL) {
    gauge.setValue(LEVEL);
    Point p;
    for( int i = 0 ; i < detail + 1 ; i++ ) {
        p = particles.get(i);
        p.y = size * (1d - LEVEL);
        p.originalY = p.y;
    }
    text.setText(String.format(Locale.US, "%.0f%%", 100 * LEVEL));
    text.setX((size - text.getLayoutBounds().getWidth()) * 0.5);
    text.setFill(LEVEL < 0.45 ? darkerColor : brighterColor);
    impulse();
}
Therefore you just have to add the call the impulse() method to the setLevel() method and remove the following code from the AnimationTimer
if (now > lastImpulseCall + impulseInterval) {
    impulse();
    lastImpulseCall = now;
}


Please keep in mind that this is (like the name says) a Fun Component and that there is always room for improvements but it's good enough to give you some ideas...

And that's it for today, enjoy the upcoming weekend and keep coding...

Wednesday, February 24, 2016

Building a multi gauge with Medusa

Today I will show you how to build what I call a multi gauge, so a gauge that shows more than one single value but for example shows three different values. This comes in handy when you don't have much space to visualize data.

Here is an example of such a gauge
So in this case the RPM is the most important value that you check very often where the temperature and oil are not checked that often which explains the smaller size. To build something similar with Medusa we need to take three Gauge objects.
  • 1x GaugeSkin (RPM)
  • 2x Horizontal Skins (TEMP and OIL)
Overlaying those controls in one area is not a big problem because the standard background fill of the Medusa gauges is transparent, so we simply have to put all three gauges in one layout container.
So the idea is as follows, we create a new class named MultiGauge that extends Region. Therefore I use the following skeleton..

public class MultiGauge extends Region {
    private static final double  PREFERRED_WIDTH  = 250;
    private static final double  PREFERRED_HEIGHT = 250;
    private static final double  MINIMUM_WIDTH    = 50;
    private static final double  MINIMUM_HEIGHT   = 50;
    private static final double  MAXIMUM_WIDTH    = 1024;
    private static final double  MAXIMUM_HEIGHT   = 1024;
    private static       double  aspectRatio;
    private              boolean keepAspect;
    private              double  size;
    private              double  width;
    private              double  height;
    private              Pane    pane;


    // ******************** Constructors **************************************
    public MultiGauge() {
        getStylesheets().add(MultiGauge.class.getResource("styles.css").toExternalForm());
        getStyleClass().add(getClass().getSimpleName().toLowerCase());
        aspectRatio = PREFERRED_HEIGHT / PREFERRED_WIDTH;
        keepAspect  = true;
        init();
        initGraphics();
        registerListeners();
    }


    // ******************** Initialization ************************************
    private void init() {
        if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 ||
            Double.compare(getWidth(), 0.0) <= 0 || Double.compare(getHeight(), 0.0) <= 0) {
            if (getPrefWidth() > 0 && getPrefHeight() > 0) {
                setPrefSize(getPrefWidth(), getPrefHeight());
            } else {
                setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
            }
        }

        if (Double.compare(getMinWidth(), 0.0) <= 0 || Double.compare(getMinHeight(), 0.0) <= 0) {
            setMinSize(MINIMUM_WIDTH, MINIMUM_HEIGHT);
        }

        if (Double.compare(getMaxWidth(), 0.0) <= 0 || Double.compare(getMaxHeight(), 0.0) <= 0) {
            setMaxSize(MAXIMUM_WIDTH, MAXIMUM_HEIGHT);
        }
    }

    private void initGraphics() {

        pane = new Pane();

        getChildren().setAll(pane);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());
    }


    // ******************** Methods *******************************************
    private void handleControlPropertyChanged(final String PROPERTY) {
        if ("".equals(PROPERTY)) {

        }
    }

    
    // ******************** Resizing ******************************************
    private void resize() {
        width  = getWidth() - getInsets().getLeft() - getInsets().getRight();
        height = getHeight() - getInsets().getTop() - getInsets().getBottom();
        size   = width < height ? width : height;

        if (keepAspect) {
            if (aspectRatio * width > height) {
                width = 1 / (aspectRatio / height);
            } else if (1 / (aspectRatio / height) > width) {
                height = aspectRatio * width;
            }
        }
        
        if (width > 0 && height > 0) {
            // Use for square controls where width == height            pane.setMaxSize(size, size);
            pane.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5);

            // Use for rectangular controls width != height            pane.setMaxSize(width, height);
            pane.relocate((getWidth() - width) * 0.5, (getHeight() - height) * 0.5);
            
        }
    }
}

With this template you could easily compose a new control out of existing controls. So the main work we have to do is to create the three gauges and put them in the right location in the new control.
Let's start with the rpmGauge control. Because I've explained styling a Medusa gauge already in my blogpost "Building a fuel gauge using Medusa" I won't go through all the details but directly give you the code, here it is...

rpmGauge = GaugeBuilder.create()
                       .borderPaint(Color.WHITE)
                       .foregroundBaseColor(Color.WHITE)
                       .prefSize(400, 400)
                       .startAngle(290)
                       .angleRange(220)
                       .minValue(0)
                       .maxValue(4000)
                       .valueVisible(false)
                       .minorTickMarksVisible(false)
                       .majorTickMarkType(TickMarkType.BOX)
                       .mediumTickMarkType(TickMarkType.BOX)
                       .title("RPM\nx100")
                       .needleShape(NeedleShape.ROUND)
                       .needleSize(NeedleSize.THICK)
                       .needleColor(Color.rgb(234, 67, 38))
                       .knobColor(Gauge.DARK_COLOR)
                       .customTickLabelsEnabled(true)
                       .customTickLabelFontSize(40)
                       .customTickLabels("0", "", "10", "", "20", "", "30", "", "40")
                       .animated(true)
                       .build();

We add this code block to the initGraphics() method and we also have to add the rpmGauge as a member variable.
To give you an idea how it would look like, here is a little screenshot...


So the next thing we have to do is adding the temperature and oil gauge. Long story short, here is the code...

tempGauge = GaugeBuilder.create()
                        .skinType(SkinType.HORIZONTAL)
                        .prefSize(170, 170)
                        .autoScale(false)
                        .foregroundBaseColor(Color.WHITE)
                        .title("TEMP")
                        .valueVisible(false)
                        .angleRange(90)
                        .minValue(100)
                        .maxValue(250)
                        .needleShape(NeedleShape.ROUND)
                        .needleSize(NeedleSize.THICK)
                        .needleColor(Color.rgb(234, 67, 38))
                        .minorTickMarksVisible(false)
                        .mediumTickMarksVisible(false)
                        .majorTickMarkType(TickMarkType.BOX)
                        .knobColor(Gauge.DARK_COLOR)
                        .customTickLabelsEnabled(true)
                        .customTickLabelFontSize(36)
                        .customTickLabels("100", "", "", "", "", "", "", "175", "", "", "", "", "", "", "", "250")
                        .animated(true)
                        .build();

oilGauge = GaugeBuilder.create()
                       .skinType(SkinType.HORIZONTAL)
                       .prefSize(170, 170)
                       .foregroundBaseColor(Color.WHITE)
                       .title("OIL")
                       .valueVisible(false)
                       .angleRange(90)
                       .needleShape(NeedleShape.ROUND)
                       .needleSize(NeedleSize.THICK)
                       .needleColor(Color.rgb(234, 67, 38))
                       .minorTickMarksVisible(false)
                       .mediumTickMarksVisible(false)
                       .majorTickMarkType(TickMarkType.BOX)
                       .knobColor(Gauge.DARK_COLOR)
                       .customTickLabelsEnabled(true)
                       .customTickLabelFontSize(36)
                       .customTickLabels("0", "", "", "", "", "50", "", "", "", "", "100")
                       .animated(true)
                       .build();

After adding both gauges to our control it will now look like this...


It's not perfect but also not too bad...and it works :)
The only thing that we now miss is how to resize and relocate the gauges correctly, well that's pretty simple and here is the related code from the resize() method of our MultiGauge control...

private void resize() {
    double width  = getWidth() - getInsets().getLeft() - getInsets().getRight();
    double height = getHeight() - getInsets().getTop() - getInsets().getBottom();
    size          = width < height ? width : height;

    if (size > 0) {
        pane.setMaxSize(size, size);
        pane.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5);

        rpmGauge.setPrefSize(size, size);

        tempGauge.setPrefSize(size * 0.425, size * 0.425);
        tempGauge.relocate(size * 0.1, size * 0.5625);

        oilGauge.setPrefSize(size * 0.425, size * 0.425);
        oilGauge.relocate(size * 0.475, size * 0.5625);
    }
}

As you can see it is really simple to resize and relocate the controls within our MultiGauge control. To get access to the gauges I simply added three get methods for each gauge and that's all it needs :)

You will find the complete example on github.

That's it for today...so keep coding... :)