Monday, February 8, 2016

Building a compass with Medusa

Here we go again...
A couple of days ago the question came up if it is possible to create a compass control with the Medusa gauge? To be honest a dedicated control would fit better but it is possible. The problem with a compass is that it has a range from 0-359 degrees and usually there is something like zero-crossing. Means that the needle always takes the shortest distance to the new position, no matter where it is. So if the needle is pointing to 10 degrees and the next value is 340 degrees the needle would rotate counter clockwise to the new value which is different from the usual gauge behavior where the needle would go clockwise.

I've implemented an experimental feature in Medusa 3.1 named needleBehavior which could either be NeedleBehavior.STANDARD or NeedleBehavior.OPTIMIZED. Only the GaugeSkin makes use of the behavior and also only if it is animated (otherwise it doesn't make sense).
I've only tested it with the following control so be warned to NOT use it with the standard gauges because I'm pretty sure it will lead to wrong behavior.
But long story short, let's take a look at the gauge setup for a compass like control...
Gauge gauge = GaugeBuilder.create()
                          .minValue(0)
                          .maxValue(359)
                          .startAngle(180)
                          .angleRange(360)
                          .build();
So first of all we created a normal gauge with a range from 0-359. It should have 0 degree on top which means we set the startAngle to 180 and the angleRange to 360 for a full circle.



Now we have to disable the autoscaling feature to get exactly our required range from 0-359 and replace the tick labels with our custom ones ("N", "E", "S", "W") in a bigger size.
Gauge gauge = GaugeBuilder.create()
                          .minValue(0)
                          .maxValue(359)
                          .startAngle(180)
                          .angleRange(360)
                          .autoScale(false)
                          .customTickLabelsEnabled(true)
                          .customTickLabels("N", "", "", "", "", "", "", "", "",
                                            "E", "", "", "", "", "", "", "", "",
                                            "S", "", "", "", "", "", "", "", "",
                                            "W", "", "", "", "", "", "", "", "")
                          .customTickLabelFontSize(72)
                          .build();
This will give us the following visualization...


Well that's not bad but the tick marks are annoying so let's get rid of that and the needle could also be a bit bigger.
Gauge gauge = GaugeBuilder.create()                             
                          .minValue(0)
                          .maxValue(359)
                          .startAngle(180)
                          .angleRange(360)
                          .autoScale(false)
                          .customTickLabelsEnabled(true)
                          .customTickLabels("N", "", "", "", "", "", "", "", "",
                                            "E", "", "", "", "", "", "", "", "",
                                            "S", "", "", "", "", "", "", "", "",
                                            "W", "", "", "", "", "", "", "", "")
                          .customTickLabelFontSize(72)
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarksVisible(false)
                          .needleType(NeedleType.FAT)
                          .build();
And with this modifications it will look like this...


Ok, we are getting there, so the value text has to be removed, the knob and needle could have a more flat style and a border around the control would also be nice...well let's do it...
Gauge gauge = GaugeBuilder.create()
                          .minValue(0)
                          .maxValue(359)
                          .startAngle(180)
                          .angleRange(360)
                          .autoScale(false)
                          .customTickLabelsEnabled(true)
                          .customTickLabels("N", "", "", "", "", "", "", "", "",
                                            "E", "", "", "", "", "", "", "", "",
                                            "S", "", "", "", "", "", "", "", "",
                                            "W", "", "", "", "", "", "", "", "")
                          .customTickLabelFontSize(72)
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarksVisible(false)
                          .valueVisible(false)
                          .needleType(NeedleType.FAT)
                          .needleShape(NeedleShape.FLAT)
                          .knobType(KnobType.FLAT)
                          .knobColor(Gauge.DARK_COLOR)
                          .borderPaint(Gauge.DARK_COLOR)
                          .build();
So with this modifications in place our compass looks not too bad...


So now let's switch on the animation and needleBehavior like follows...
Gauge gauge = GaugeBuilder.create()
                          .minValue(0)
                          .maxValue(359)
                          .startAngle(180)
                          .angleRange(360)
                          .autoScale(false)
                          .customTickLabelsEnabled(true)
                          .customTickLabels("N", "", "", "", "", "", "", "", "",
                                            "E", "", "", "", "", "", "", "", "",
                                            "S", "", "", "", "", "", "", "", "",
                                            "W", "", "", "", "", "", "", "", "")
                          .customTickLabelFontSize(72)
                          .minorTickMarksVisible(false)
                          .mediumTickMarksVisible(false)
                          .majorTickMarksVisible(false)
                          .valueVisible(false)
                          .needleType(NeedleType.FAT)
                          .needleShape(NeedleShape.FLAT)
                          .knobType(KnobType.FLAT)
                          .knobColor(Gauge.DARK_COLOR)
                          .borderPaint(Gauge.DARK_COLOR)
                          .animated(true)
                          .animationDuration(500)
                          .needleBehavior(NeedleBehavior.OPTIMIZED)
                          .build();
If you now set the value of the gauge you will (hopefully) see that the needle always takes the optimized way to the next value (means the shortest angle distance). 

ATTENTION:
Keep in mind that as soon as you hook this control up to a real device you should always switch of the animation because values from real devices might come in at high rate.

Because we switched of the value text you might want to add a separate Label that contains the current value of the gauge. Therefore you simply add it as follows...
Label value = new Label("0°");
value.setFont(Fonts.latoBold(72));
value.setAlignment(Pos.CENTER);
And to update the Label with the value of the gauge we simply add a listener to the valueProperty() of the gauge like this...
gauge.valueProperty().addListener(o -> {
    value.setText(String.format(Locale.US, "%.0f°", gauge.getValue()));
});
If you put those controls in a VBox you will get something like this...


And that's it...not too bad right :)

The code can be found on github...

Keep coding...

2 comments: