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... :)

6 comments:

  1. can i use this on my Desktop Programs?? any copyright issue?

    ReplyDelete
    Replies
    1. Apache 2.0...so feel free to use it...that's the main intention of this library :)

      Delete
  2. It is fantastic library! But very sad, that impossible use this library in android studio projects..

    ReplyDelete
  3. As always amazing job, thanks for sharing

    ReplyDelete