Wednesday, January 27, 2016

Building a fuel gauge using Medusa

Hi there,

Today I will show you how you could build a simple fuel gauge with Medusa. To give you an idea on what I'm talking about, here is an image of a VDO fuel gauge that you can buy...



To get as close as possible to this design we'll use a horizontal gauge that comes with an angle range of 180 degrees. By using the GaugeBuilder it will look like this...

Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .build();
It will look as follows...



Because most of the car dashboards are colored dark we'll adjust the colors as follows...

Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .build();
To get a nice result we also have to set the background of our container (e.g. a StackPane) to a nice dark gradient. This will give us the following visualization...



In the next step we have to adjust the scale (from 0.0 to 1.0), hide the value text and add a section with a red color for the reserve (from 0.0 to 0.2). The code to realize this looks like this...
Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .maxValue(1.0)
                          .valueVisible(false)
                          .sectionsVisible(true)
                          .sections(new Section(0.0, 0.2, Color.rgb(255, 10, 1)))
                          .build();
which will make the gauge look like follows...



That's not bad but now let's get rid of the minor and medium tick marks, add a title ("FUEL") and set another tick mark type for the major tick marks (TickMarkType.BOX)
Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .maxValue(1.0)
                          .valueVisible(false)
                          .sectionsVisible(true)
                          .sections(new Section(0.0, 0.2, Color.rgb(255, 10, 1)))
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarkType(TickMarkType.BOX)
                          .title("FUEL")
                          .build();
So far so good, now it should look like this...



In principle this is ok except the tick labels that show only "0" and "1" because the tick label decimals are set to 0. If we set them to 1 like in the following code...
Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .maxValue(1.0)
                          .valueVisible(false)
                          .sectionsVisible(true)
                          .sections(new Section(0.0, 0.2, Color.rgb(255, 10, 1)))
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarkType(TickMarkType.BOX)
                          .title("FUEL")
                          .tickLabelDecimals(1)
                          .build();
we will get the following result...



So with this we are getting there, maybe the needle could be a bit bigger and with a more round look. Also the color of the needle could be more orange and a shadow would also be nice. So here you go...
Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .maxValue(1.0)
                          .valueVisible(false)
                          .sectionsVisible(true)
                          .sections(new Section(0.0, 0.2, Color.rgb(255, 10, 1)))
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarkType(TickMarkType.BOX)
                          .title("FUEL")
                          .tickLabelDecimals(1)
                          .needleSize(NeedleSize.THICK)
                          .needleShape(NeedleShape.ROUND)
                          .needleColor(Color.rgb(255, 61, 10))
                          .shadowsEnabled(true)
                          .build();
And the result will look like follows...



Well that is ok'ish but still the tick labels are a bit confusing. I would prefer something like "E" at the lower end of the scale, "F" at the upper end and "1/2" in the middle but how to realize that? I've added the ability to use custom tick labels and used a very simple implementation means it's a simple list of Strings. So we have 11 tick labels here which means we'll need a list of 11 Strings with the appropriate labels. To get it even closer to the original one we set the angle range to 90 degrees. So the final code will look like this...
Gauge gauge = GaugeBuilder.create()
                          .skinType(SkinType.HORIZONTAL)
                          .prefSize(500, 250)
                          .foregroundBaseColor(Color.rgb(249, 249, 249))
                          .knobColor(Color.BLACK)
                          .maxValue(1.0)
                          .valueVisible(false)
                          .sectionsVisible(true)
                          .sections(new Section(0.0, 0.2, Color.rgb(255, 10, 1)))
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarkType(TickMarkType.BOX)
                          .title("FUEL")
                          .needleSize(NeedleSize.THICK)
                          .needleShape(NeedleShape.ROUND)
                          .needleColor(Color.rgb(255, 61, 10))
                          .shadowsEnabled(true)
                          .angleRange(90)
                          .customTickLabelsEnabled(true)
                          .customTickLabels("E", "", "", "", "", "1/2", "", "", "", "", "F")
                          .build();
and this will give us the following result...



So that's nearly all you can do with Medusa but if you would like to replace the text "FUEL" with an icon you could easily put the icon (a JavaFX Region styled with CSS) and the gauge in a StackPane. And because the Medusa gauge comes with a transparent background you will see the icon. To do so you simply have to remove the .title("FUEL") line from the code above and add the following code to your start() method.
Region    fuelIcon = new Region();
fuelIcon.getStyleClass().setAll("icon");

StackPane pane = new StackPane(fuelIcon, gauge);
pane.setPadding(new Insets(10));
LinearGradient gradient = new LinearGradient(0, 0, 0, pane.getLayoutBounds().getHeight(),
                                             false, CycleMethod.NO_CYCLE,
                                             new Stop(0.0, Color.rgb(38, 38, 38)),
                                             new Stop(1.0, Color.rgb(15, 15, 15)));
pane.setBackground(new Background(new BackgroundFill(gradient, CornerRadii.EMPTY, Insets.EMPTY)));

Scene scene = new Scene(pane);
scene.getStylesheets().add(Test.class.getResource("styles.css").toExternalForm()); 
As you can see we add a css file named "styles.css" to the scene and the set the ".icon" style class for the fuelIcon region. The css file looks like this...
.icon {
    -fx-max-width        : 36px;
    -fx-max-height       : 41px;
    -fx-background-color : rgb(249, 249, 249);
    -fx-scale-shape      : true;
    -fx-shape            : "M 25.775 17.025 L 25.775 8.645 L 28.3633 11.2324 L 29.5547 17.6875
                            C 29.6035 18.0293 29.7988 18.332 30.0918 18.5273 L 33.2949 20.666
                            L 33.2949 30.6855 C 33.2754 31.2617 33.0508 31.9551 32.0449 31.9551
                            C 31.0293 31.9551 30.7949 31.2617 30.7754 30.6953 L 30.7754 23.166
                            C 30.7754 19.0669 27.9706 17.4574 25.775 17.025 ZM 25.7754 4.6699
                            C 25.7754 2.4141 23.9492 0.5879 21.6934 0.5879 L 9.7793 0.5879
                            C 7.5332 0.5879 5.6973 2.4141 5.6973 4.6699 L 5.6973 5.6074
                            L 5.6973 29.1328 L 5.6973 33.2148 L 9.7793 33.2148 L 21.6934 33.2148
                            L 25.7754 33.2148 L 25.7754 29.1328 L 25.775 19.605
                            C 26.8895 19.9367 28.2754 20.8192 28.2754 23.166 L 28.2754 30.6953
                            C 28.2754 32.209 29.2813 34.4648 32.0449 34.4648
                            C 34.7988 34.4648 35.8047 32.209 35.8047 30.6953 L 35.8047 20.7246
                            C 35.8145 20.7051 35.8145 20.6758 35.8145 20.666
                            C 35.8145 20.5488 35.7949 20.4414 35.7656 20.334 L 34.5352 12.9316
                            C 34.4766 12.5801 34.2813 12.2773 33.9883 12.0918 L 30.3262 9.6504
                            L 25.775 5.1 L 25.7754 4.6699 ZM 22.0156 5.8613 L 22.0156 12.1211
                            C 22.0156 13.166 21.166 14.0059 20.1309 14.0059 L 11.3516 14.0059
                            C 10.3066 14.0059 9.4668 13.166 9.4668 12.1211 L 9.4668 5.8613
                            C 9.4668 4.8164 10.3066 3.9766 11.3516 3.9766 L 20.1309 3.9766
                            C 21.166 3.9766 22.0156 4.8164 22.0156 5.8613 ZM 29.2227 35.7148
                            L 2.25 35.7148 C 1.3809 35.7148 0.6777 36.418 0.6777 37.2871
                            L 0.6777 38.2246 L 0.6777 39.1719 L 0.6777 40.7344 L 2.25 40.7344
                            L 29.2227 40.7344 L 30.7949 40.7344 L 30.7949 39.1719 L 30.7949 38.2246
                            L 30.7949 37.2871 C 30.7949 36.418 30.0918 35.7148 29.2227 35.7148 Z ";
}
As you can see we make use of SVG in the -fx-shape css property and set it's max size to 36x41 pixels. With this modifications in place our fuel gauge will then look like this...



et voilĂ  here you go, a fuel gauge...
And if you prefer a vertical gauge instead of the horizontal one you can simply exchange the call .skinType(SkinType.HORIZONTAL) with .skinType(SkinType.VERTICAL) in the example above and you will get something like this...



Please find the code for this demo over at github...

That's it for today, I hope you enjoy Java(FX) as much as I do, keep coding...

10 comments:

  1. That. Actually. Is pretty damn impressive, Gerrit!

    ReplyDelete
    Replies
    1. Thanx, glad you like it my friend :)

      Delete
  2. Very elegant in both code and aesthetic--great job!

    ReplyDelete
  3. I probably don't have any need for this in stuff I'm doing, but I wish I did ! Very, very impressive. Great work.

    ReplyDelete
  4. Great work Gerrit! I would like to ask you if I can use a custom "needle" shape, e.g. a treble clef (even in SVG format).
    Thanks for your efforts :D

    ReplyDelete
    Replies
    1. Hi Antonello,
      To answer your question...nope it's not possible at the moment. Custom needle shapes are on the list but not that easy to implement for different kinds of gauges. It might come in the future but at the moment you have to live with the given needle, sorry :(
      Cheers,
      Gerrit

      Delete
    2. Please forgive my delay in answering, your needle are perfect in stuff I'm doing.
      Quetion: What about your documentation? The javadoc.jar package contains only a manifest.
      Thanks for reply :)

      Delete
    3. Hi Antonello,
      Well documentation takes time...time that I don't have atm, so I created demos that I also use for testing. I hope to find some time in the future to add some javadoc.
      Cheers,
      Gerrit

      Delete
    4. So I've started docmenting :)

      Delete
    5. I'm glad to hear you talking about docs! Good job :)
      Cheers

      Delete