/*
* Copyright (c) 2016 by Gerrit Grunwald
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.hansolo.medusa.skins;
import eu.hansolo.medusa.Fonts;
import eu.hansolo.medusa.Gauge;
import eu.hansolo.medusa.Gauge.ScaleDirection;
import eu.hansolo.medusa.Section;
import eu.hansolo.medusa.tools.Helper;
import javafx.beans.InvalidationListener;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.CacheHint;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Rotate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Created by hansolo on 16.01.16.
*/
public class IndicatorSkin extends GaugeSkinBase {
protected static final double PREFERRED_WIDTH = 250;
protected static final double PREFERRED_HEIGHT = 165;
protected static final double MINIMUM_WIDTH = 50;
protected static final double MINIMUM_HEIGHT = 50;
protected static final double MAXIMUM_WIDTH = 1024;
protected static final double MAXIMUM_HEIGHT = 1024;
private static final double ASPECT_RATIO = 0.59375;
private double width;
private double height;
private double oldValue;
private Arc barBackground;
private Pane sectionLayer;
private Arc bar;
private Path needle;
private MoveTo needleMoveTo1;
private CubicCurveTo needleCubicCurveTo2;
private CubicCurveTo needleCubicCurveTo3;
private CubicCurveTo needleCubicCurveTo4;
private CubicCurveTo needleCubicCurveTo5;
private CubicCurveTo needleCubicCurveTo6;
private CubicCurveTo needleCubicCurveTo7;
private ClosePath needleClosePath8;
private Rotate needleRotate;
private Text minValueText;
private Text maxValueText;
private Text titleText;
private Pane pane;
private double angleRange;
private double minValue;
private double range;
private double angleStep;
private double startAngle;
private boolean colorGradientEnabled;
private int noOfGradientStops;
private boolean sectionsAlwaysVisible;
private boolean sectionsVisible;
private List<Section> sections;
private Tooltip needleTooltip;
private String formatString;
private Locale locale;
private Color barColor;
private Tooltip barTooltip;
private InvalidationListener currentValueListener;
private InvalidationListener sectionAlwaysVisibleListener;
// ******************** Constructors **************************************
public IndicatorSkin(Gauge gauge) {
super(gauge);
if (gauge.isAutoScale()) gauge.calcAutoScale();
angleRange = Helper.clamp(90.0, 180.0, gauge.getAngleRange());
startAngle = getStartAngle();
oldValue = gauge.getValue();
minValue = gauge.getMinValue();
range = gauge.getRange();
angleStep = angleRange / range;
colorGradientEnabled = gauge.isGradientBarEnabled();
noOfGradientStops = gauge.getGradientBarStops().size();
sectionsAlwaysVisible = gauge.getSectionsAlwaysVisible();
sectionsVisible = gauge.getSectionsVisible();
sections = gauge.getSections();
formatString = new StringBuilder("%.").append(Integer.toString(gauge.getDecimals())).append("f").toString();
locale = gauge.getLocale();
barColor = gauge.getBarColor();
currentValueListener = o -> rotateNeedle(gauge.getCurrentValue());
sectionAlwaysVisibleListener = o -> bar.setVisible(!gauge.getSectionsAlwaysVisible());
initGraphics();
registerListeners();
rotateNeedle(gauge.getCurrentValue());
}
// ******************** Initialization ************************************
private void initGraphics() {
// Set initial size
if (Double.compare(gauge.getPrefWidth(), 0.0) <= 0 || Double.compare(gauge.getPrefHeight(), 0.0) <= 0 ||
Double.compare(gauge.getWidth(), 0.0) <= 0 || Double.compare(gauge.getHeight(), 0.0) <= 0) {
if (gauge.getPrefWidth() > 0 && gauge.getPrefHeight() > 0) {
gauge.setPrefSize(gauge.getPrefWidth(), gauge.getPrefHeight());
} else {
gauge.setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
}
}
barBackground = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.696, PREFERRED_WIDTH * 0.275, PREFERRED_WIDTH * 0.275, angleRange * 0.5 + 90, -angleRange);
barBackground.setType(ArcType.OPEN);
barBackground.setStroke(gauge.getBarBackgroundColor());
barBackground.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
barBackground.setStrokeLineCap(StrokeLineCap.BUTT);
barBackground.setFill(null);
sectionLayer = new Pane();
sectionLayer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
bar = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.696, PREFERRED_WIDTH * 0.275, PREFERRED_WIDTH * 0.275, angleRange * 0.5 + 90, 0);
bar.setType(ArcType.OPEN);
bar.setStroke(gauge.getBarColor());
bar.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
bar.setStrokeLineCap(StrokeLineCap.BUTT);
bar.setFill(null);
//bar.setMouseTransparent(sectionsAlwaysVisible ? true : false);
bar.setVisible(!sectionsAlwaysVisible);
needleRotate = new Rotate((gauge.getValue() - oldValue - minValue) * angleStep);
needleMoveTo1 = new MoveTo();
needleCubicCurveTo2 = new CubicCurveTo();
needleCubicCurveTo3 = new CubicCurveTo();
needleCubicCurveTo4 = new CubicCurveTo();
needleCubicCurveTo5 = new CubicCurveTo();
needleCubicCurveTo6 = new CubicCurveTo();
needleCubicCurveTo7 = new CubicCurveTo();
needleClosePath8 = new ClosePath();
needle = new Path(needleMoveTo1, needleCubicCurveTo2, needleCubicCurveTo3, needleCubicCurveTo4, needleCubicCurveTo5, needleCubicCurveTo6, needleCubicCurveTo7, needleClosePath8);
needle.setFillRule(FillRule.EVEN_ODD);
needle.getTransforms().setAll(needleRotate);
needle.setFill(gauge.getNeedleColor());
needle.setPickOnBounds(false);
needle.setStrokeType(StrokeType.INSIDE);
needle.setStrokeWidth(1);
needle.setStroke(gauge.getBackgroundPaint());
needleTooltip = new Tooltip(String.format(locale, formatString, gauge.getValue()));
needleTooltip.setTextAlignment(TextAlignment.CENTER);
Tooltip.install(needle, needleTooltip);
minValueText = new Text(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMinValue()));
minValueText.setFill(gauge.getTitleColor());
Helper.enableNode(minValueText, gauge.getTickLabelsVisible());
maxValueText = new Text(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMaxValue()));
maxValueText.setFill(gauge.getTitleColor());
Helper.enableNode(maxValueText, gauge.getTickLabelsVisible());
titleText = new Text(gauge.getTitle());
titleText.setFill(gauge.getTitleColor());
Helper.enableNode(titleText, !gauge.getTitle().isEmpty());
if (!sections.isEmpty() && sectionsVisible && !sectionsAlwaysVisible) {
barTooltip = new Tooltip();
barTooltip.setTextAlignment(TextAlignment.CENTER);
Tooltip.install(bar, barTooltip);
}
pane = new Pane(barBackground, sectionLayer, bar, needle, minValueText, maxValueText, titleText);
pane.setBorder(new Border(new BorderStroke(gauge.getBorderPaint(), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(gauge.getBorderWidth()))));
pane.setBackground(new Background(new BackgroundFill(gauge.getBackgroundPaint(), CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(pane);
}
@Override protected void registerListeners() {
super.registerListeners();
gauge.currentValueProperty().addListener(currentValueListener);
gauge.sectionsAlwaysVisibleProperty().addListener(sectionAlwaysVisibleListener);
}
// ******************** Methods *******************************************
@Override protected void handleEvents(final String EVENT_TYPE) {
super.handleEvents(EVENT_TYPE);
if ("RECALC".equals(EVENT_TYPE)) {
angleRange = Helper.clamp(90.0, 180.0, gauge.getAngleRange());
startAngle = getStartAngle();
minValue = gauge.getMinValue();
range = gauge.getRange();
sections = gauge.getSections();
angleStep = angleRange / range;
redraw();
rotateNeedle(gauge.getCurrentValue());
} else if ("FINISHED".equals(EVENT_TYPE)) {
needleTooltip.setText(String.format(locale, formatString, gauge.getValue()));
double value = gauge.getValue();
if (gauge.isValueVisible()) {
Bounds bounds = pane.localToScreen(pane.getBoundsInLocal());
double xFactor = value > gauge.getRange() * 0.8 ? 0.0 : 0.25;
double tooltipAngle = value * angleStep;
double sinValue = Math.sin(Math.toRadians(180 + angleRange * 0.5 - tooltipAngle));
double cosValue = Math.cos(Math.toRadians(180 + angleRange * 0.5 - tooltipAngle));
double needleTipX = bounds.getMinX() + bounds.getWidth() * 0.5 + bounds.getHeight() * sinValue;
double needleTipY = bounds.getMinY() + bounds.getHeight() * 0.72 + bounds.getHeight() * cosValue;
needleTooltip.show(needle, needleTipX, needleTipY);
needleTooltip.setAnchorX(needleTooltip.getX() - needleTooltip.getWidth() * xFactor);
}
if (sections.isEmpty() || sectionsAlwaysVisible) return;
for (Section section : sections) {
if (section.contains(value)) {
barTooltip.setText(section.getText());
break;
}
}
} else if ("VISIBILITY".equals(EVENT_TYPE)) {
Helper.enableNode(titleText, !gauge.getTitle().isEmpty());
}
}
private double getStartAngle() {
ScaleDirection scaleDirection = gauge.getScaleDirection();
switch(scaleDirection) {
//case COUNTER_CLOCKWISE: return 180 - angleRange * 0.5;
case CLOCKWISE :
default : return 180 + angleRange * 0.5;
}
}
private void rotateNeedle(final double VALUE) {
double needleStartAngle = angleRange * 0.5;
double targetAngle = (VALUE - minValue) * angleStep - needleStartAngle;
targetAngle = Helper.clamp(-needleStartAngle, -needleStartAngle + angleRange, targetAngle);
needleRotate.setAngle(targetAngle);
bar.setLength(-(gauge.getCurrentValue() - minValue) * angleStep);
setBarColor(VALUE);
}
private void setBarColor(final double VALUE) {
if (!sectionsVisible && !colorGradientEnabled) {
bar.setStroke(barColor);
} else if (colorGradientEnabled && noOfGradientStops > 1) {
bar.setStroke(gauge.getGradientLookup().getColorAt((VALUE - minValue) / range));
} else {
bar.setStroke(barColor);
for (Section section : sections) {
if (section.contains(VALUE)) {
bar.setStroke(section.getColor());
break;
}
}
}
}
@Override public void dispose() {
gauge.currentValueProperty().removeListener(currentValueListener);
gauge.sectionsAlwaysVisibleProperty().removeListener(sectionAlwaysVisibleListener);
super.dispose();
}
// ******************** Resizing ******************************************
private void resizeStaticText() {
double maxWidth = width * 0.28472222;
double fontSize = height * 0.12631579;
minValueText.setFont(Fonts.latoRegular(fontSize));
if (minValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(minValueText, maxWidth, fontSize); }
minValueText.relocate((width * 0.28472222) - minValueText.getLayoutBounds().getWidth(), height * 0.885);
maxValueText.setFont(Fonts.latoRegular(fontSize));
if (maxValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(maxValueText, maxWidth, fontSize); }
maxValueText.relocate(width * 0.71527778, height * 0.885);
maxWidth = width * 0.9;
titleText.setFont(Fonts.latoRegular(fontSize));
if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, fontSize); }
titleText.relocate((width - titleText.getLayoutBounds().getWidth()) * 0.5, height * 0.95);
}
@Override protected void resize() {
width = gauge.getWidth() - gauge.getInsets().getLeft() - gauge.getInsets().getRight();
height = gauge.getHeight() - gauge.getInsets().getTop() - gauge.getInsets().getBottom();
if (ASPECT_RATIO * width > height) {
width = 1 / (ASPECT_RATIO / height);
} else if (1 / (ASPECT_RATIO / height) > width) {
height = ASPECT_RATIO * width;
}
if (width > 0 && height > 0) {
double centerX = width * 0.5;
double centerY = height * 0.85;
double barRadius = height * 0.54210526;
double barWidth = width * 0.28472222;
pane.setMaxSize(width, height);
pane.relocate((gauge.getWidth() - width) * 0.5, (gauge.getHeight() - height) * 0.5);
barBackground.setCenterX(centerX);
barBackground.setCenterY(centerY);
barBackground.setRadiusX(barRadius);
barBackground.setRadiusY(barRadius);
barBackground.setStrokeWidth(barWidth);
barBackground.setStartAngle(angleRange * 0.5 + 90);
barBackground.setLength(-angleRange);
if (sectionsVisible && sectionsAlwaysVisible) {
sectionLayer.setPrefSize(width, height);
drawSections();
}
bar.setCenterX(centerX);
bar.setCenterY(centerY);
bar.setRadiusX(barRadius);
bar.setRadiusY(barRadius);
bar.setStrokeWidth(barWidth);
bar.setStartAngle(angleRange * 0.5 + 90);
bar.setLength(-(gauge.getCurrentValue() - minValue) * angleStep);
double needleWidth = height * 0.13157895;
double needleHeight = height * 0.91315789;
needle.setCache(true);
needleMoveTo1.setX(0.0); needleMoveTo1.setY(0.927953890489914 * needleHeight);
needleCubicCurveTo2.setControlX1(0); needleCubicCurveTo2.setControlY1(0.968299711815562 * needleHeight);
needleCubicCurveTo2.setControlX2(0.22 * needleWidth); needleCubicCurveTo2.setControlY2(needleHeight);
needleCubicCurveTo2.setX(0.5 * needleWidth); needleCubicCurveTo2.setY(needleHeight);
needleCubicCurveTo3.setControlX1(0.78 * needleWidth); needleCubicCurveTo3.setControlY1(needleHeight);
needleCubicCurveTo3.setControlX2(needleWidth); needleCubicCurveTo3.setControlY2(0.968299711815562 * needleHeight);
needleCubicCurveTo3.setX(needleWidth); needleCubicCurveTo3.setY(0.927953890489914 * needleHeight);
needleCubicCurveTo4.setControlX1(needleWidth); needleCubicCurveTo4.setControlY1(0.92507204610951 * needleHeight);
needleCubicCurveTo4.setControlX2(0.6 * needleWidth); needleCubicCurveTo4.setControlY2(0.0144092219020173 * needleHeight);
needleCubicCurveTo4.setX(0.6 * needleWidth); needleCubicCurveTo4.setY(0.0144092219020173 * needleHeight);
needleCubicCurveTo5.setControlX1(0.6 * needleWidth); needleCubicCurveTo5.setControlY1(0.0144092219020173 * needleHeight);
needleCubicCurveTo5.setControlX2(0.58 * needleWidth); needleCubicCurveTo5.setControlY2(0);
needleCubicCurveTo5.setX(0.5 * needleWidth); needleCubicCurveTo5.setY(0);
needleCubicCurveTo6.setControlX1(0.42 * needleWidth); needleCubicCurveTo6.setControlY1(0);
needleCubicCurveTo6.setControlX2(0.4 * needleWidth); needleCubicCurveTo6.setControlY2(0.0144092219020173 * needleHeight);
needleCubicCurveTo6.setX(0.4 * needleWidth); needleCubicCurveTo6.setY(0.0144092219020173 * needleHeight);
needleCubicCurveTo7.setControlX1(0.4 * needleWidth); needleCubicCurveTo7.setControlY1(0.0144092219020173 * needleHeight);
needleCubicCurveTo7.setControlX2(0); needleCubicCurveTo7.setControlY2(0.92507204610951 * needleHeight);
needleCubicCurveTo7.setX(0); needleCubicCurveTo7.setY(0.927953890489914 * needleHeight);
needle.setCache(true);
needle.setCacheHint(CacheHint.ROTATE);
needle.relocate((width - needle.getLayoutBounds().getWidth()) * 0.5, centerY - needle.getLayoutBounds().getHeight() + needle.getLayoutBounds().getWidth() * 0.5);
needleRotate.setPivotX(needle.getLayoutBounds().getWidth() * 0.5);
needleRotate.setPivotY(needle.getLayoutBounds().getHeight() - needle.getLayoutBounds().getWidth() * 0.5);
resizeStaticText();
}
}
@Override protected void redraw() {
pane.setBorder(new Border(new BorderStroke(gauge.getBorderPaint(), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(gauge.getBorderWidth() / PREFERRED_HEIGHT * height))));
pane.setBackground(new Background(new BackgroundFill(gauge.getBackgroundPaint(), CornerRadii.EMPTY, Insets.EMPTY)));
barColor = gauge.getBarColor();
locale = gauge.getLocale();
formatString = new StringBuilder("%.").append(Integer.toString(gauge.getDecimals())).append("f").toString();
colorGradientEnabled = gauge.isGradientBarEnabled();
noOfGradientStops = gauge.getGradientBarStops().size();
sectionsVisible = gauge.getSectionsVisible();
minValueText.setText(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMinValue()));
maxValueText.setText(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMaxValue()));
resizeStaticText();
barBackground.setStroke(gauge.getBarBackgroundColor());
bar.setStroke(gauge.getBarColor());
needle.setFill(gauge.getNeedleColor());
minValueText.setVisible(gauge.getTickLabelsVisible());
maxValueText.setVisible(gauge.getTickLabelsVisible());
minValueText.setFill(gauge.getTitleColor());
maxValueText.setFill(gauge.getTitleColor());
titleText.setFill(gauge.getTitleColor());
}
private void drawSections() {
if (sections.isEmpty()) return;
sectionLayer.getChildren().clear();
double centerX = width * 0.5;
double centerY = height * 0.85;
double barRadius = height * 0.54210526;
double barWidth = width * 0.28472222;
List<Arc> sectionBars = new ArrayList<>(sections.size());
for (Section section : sections) {
Arc sectionBar = new Arc(centerX, centerY, barRadius, barRadius, angleRange * 0.5 + 90 - (section.getStart() * angleStep), -((section.getStop() - section.getStart()) - minValue) * angleStep);
sectionBar.setType(ArcType.OPEN);
sectionBar.setStroke(section.getColor());
sectionBar.setStrokeWidth(barWidth);
sectionBar.setStrokeLineCap(StrokeLineCap.BUTT);
sectionBar.setFill(null);
Tooltip sectionTooltip = new Tooltip(new StringBuilder(section.getText()).append("\n").append(String.format(Locale.US, "%.2f", section.getStart())).append(" - ").append(String.format(Locale.US, "%.2f", section.getStop())).toString());
sectionTooltip.setTextAlignment(TextAlignment.CENTER);
Tooltip.install(sectionBar, sectionTooltip);
sectionBars.add(sectionBar);
}
sectionLayer.getChildren().addAll(sectionBars);
}
}