/*
* Copyright (c) 2013 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.enzo.gauge.skin;
import eu.hansolo.enzo.common.Section;
import eu.hansolo.enzo.gauge.SimpleGauge;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.collections.ListChangeListener;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.CacheHint;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
import java.util.Locale;
/**
* Created by
* User: hansolo
* Date: 01.04.13
* Time: 17:18
*/
public class SimpleGaugeSkin extends SkinBase<SimpleGauge> implements Skin<SimpleGauge> {
private static final double PREFERRED_WIDTH = 200;
private static final double PREFERRED_HEIGHT = 200;
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 double size;
private Pane pane;
private Canvas sectionsCanvas;
private GraphicsContext sectionsCtx;
private Canvas measuredRangeCanvas;
private GraphicsContext measuredRangeCtx;
private Path needle;
private Rotate needleRotate;
private Text value;
private Text title;
private double angleStep;
private Timeline timeline;
// ******************** Constructors **************************************
public SimpleGaugeSkin(SimpleGauge gauge) {
super(gauge);
angleStep = gauge.getAngleRange() / (gauge.getMaxValue() - gauge.getMinValue());
timeline = new Timeline();
init();
initGraphics();
registerListeners();
}
// ******************** Initialization ************************************
private void init() {
if (Double.compare(getSkinnable().getPrefWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getPrefHeight(), 0.0) <= 0 ||
Double.compare(getSkinnable().getWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getHeight(), 0.0) <= 0) {
if (getSkinnable().getPrefWidth() > 0 && getSkinnable().getPrefHeight() > 0) {
getSkinnable().setPrefSize(getSkinnable().getPrefWidth(), getSkinnable().getPrefHeight());
} else {
getSkinnable().setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
}
}
if (Double.compare(getSkinnable().getMinWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMinHeight(), 0.0) <= 0) {
getSkinnable().setMinSize(MINIMUM_WIDTH, MINIMUM_HEIGHT);
}
if (Double.compare(getSkinnable().getMaxWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMaxHeight(), 0.0) <= 0) {
getSkinnable().setMaxSize(MAXIMUM_WIDTH, MAXIMUM_HEIGHT);
}
}
private void initGraphics() {
Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/opensans-semibold.ttf"), (0.06 * PREFERRED_HEIGHT)); // "OpenSans"
sectionsCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
sectionsCtx = sectionsCanvas.getGraphicsContext2D();
measuredRangeCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
measuredRangeCanvas.setManaged(getSkinnable().isMeasuredRangeVisible());
measuredRangeCanvas.setVisible(getSkinnable().isMeasuredRangeVisible());
measuredRangeCtx = measuredRangeCanvas.getGraphicsContext2D();
if (getSkinnable().getValue() < getSkinnable().getMinValue()) getSkinnable().setValue(getSkinnable().getMinValue());
if (getSkinnable().getValue() > getSkinnable().getMaxValue()) getSkinnable().setValue(getSkinnable().getMaxValue());
needleRotate = new Rotate(180 - getSkinnable().getStartAngle());
if (getSkinnable().getMinValue() < 0) {
needleRotate.setAngle(needleRotate.getAngle() + (getSkinnable().getValue() - getSkinnable().getOldValue() - getSkinnable().getMinValue()) * angleStep);
} else {
//needleRotate.setAngle(needleRotate.getAngle() + (getSkinnable().getValue() - getSkinnable().getOldValue() + getSkinnable().getMinValue()) * angleStep);
}
angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() - getSkinnable().getMinValue());
needleRotate.setAngle(needleRotate.getAngle() + (getSkinnable().getValue() - getSkinnable().getOldValue()) * angleStep);
needle = new Path();
needle.setFillRule(FillRule.EVEN_ODD);
needle.getStyleClass().setAll("needle");
needle.getTransforms().setAll(needleRotate);
value = new Text(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMinValue()) + getSkinnable().getUnit());
value.setMouseTransparent(true);
value.setTextOrigin(VPos.CENTER);
value.getStyleClass().setAll("value");
title = new Text(getSkinnable().getTitle());
title.setTextOrigin(VPos.CENTER);
title.getStyleClass().setAll("title");
// Add all nodes
pane = new Pane();
pane.getStyleClass().add("simple-gauge");
pane.getChildren().setAll(sectionsCanvas,
measuredRangeCanvas,
needle,
value,
title);
getChildren().setAll(pane);
resize();
}
private void registerListeners() {
getSkinnable().widthProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().heightProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().valueProperty().addListener(observable -> handleControlPropertyChanged("VALUE"));
getSkinnable().minValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC"));
getSkinnable().maxValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC"));
getSkinnable().titleProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().needleColorProperty().addListener(observable -> handleControlPropertyChanged("NEEDLE_COLOR"));
getSkinnable().animatedProperty().addListener(observable -> handleControlPropertyChanged("ANIMATED"));
getSkinnable().angleRangeProperty().addListener(observable -> handleControlPropertyChanged("ANGLE_RANGE"));
getSkinnable().sectionTextVisibleProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().sectionIconVisibleProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().valueTextColorProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().titleTextColorProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().sectionTextColorProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
getSkinnable().measuredRangeVisibleProperty().addListener(observable -> handleControlPropertyChanged("MEASURED_RANGE_VISIBLE"));
getSkinnable().getSections().addListener((ListChangeListener<Section>) change -> handleControlPropertyChanged("RESIZE"));
needleRotate.angleProperty().addListener(observable -> handleControlPropertyChanged("ANGLE"));
}
// ******************** Methods *******************************************
protected void handleControlPropertyChanged(final String PROPERTY) {
if ("RESIZE".equals(PROPERTY)) {
resize();
} else if ("VALUE".equals(PROPERTY)) {
rotateNeedle();
} else if ("RECALC".equals(PROPERTY)) {
angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() + getSkinnable().getMinValue());
if (getSkinnable().getMinValue() < 0) {
needleRotate.setAngle(180 - getSkinnable().getStartAngle() - (getSkinnable().getMinValue()) * angleStep);
} else {
needleRotate.setAngle(needleRotate.getAngle() + (getSkinnable().getValue() * angleStep));
}
resize();
} else if ("ANGLE".equals(PROPERTY)) {
double currentValue = (needleRotate.getAngle() + getSkinnable().getStartAngle() - 180) / angleStep + getSkinnable().getMinValue();
value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", currentValue) + getSkinnable().getUnit());
value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5);
if (value.getLayoutBounds().getWidth() > 0.45 * size) {
resizeText();
}
// Check sections
for (Section section : getSkinnable().getSections()) {
if (section.contains(currentValue)) {
section.fireSectionEvent(new Section.SectionEvent(section, null, Section.SectionEvent.ENTERING_SECTION));
break;
}
}
// Adjust minMeasured and maxMeasured values
if (currentValue < getSkinnable().getMinMeasuredValue()) {
getSkinnable().setMinMeasuredValue(currentValue);
}
if (currentValue > getSkinnable().getMaxMeasuredValue()) {
getSkinnable().setMaxMeasuredValue(currentValue);
}
if (getSkinnable().isMeasuredRangeVisible()) drawMeasuredRange();
} else if ("MEASURED_RANGE_VISIBLE".equals(PROPERTY)) {
measuredRangeCanvas.setManaged(getSkinnable().isMeasuredRangeVisible());
measuredRangeCanvas.setVisible(getSkinnable().isMeasuredRangeVisible());
}
}
public void resetNeedle() {
timeline.stop();
boolean wasAnimated = getSkinnable().isAnimated();
getSkinnable().setAnimated(false);
getSkinnable().setValue(getSkinnable().getMinValue());
if (wasAnimated) {
getSkinnable().setAnimated(true);
}
}
@Override protected double computeMinWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
return super.computeMinWidth(Math.max(MINIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
@Override protected double computeMinHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
return super.computeMinHeight(Math.max(MINIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
@Override protected double computeMaxWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
return super.computeMaxWidth(Math.min(MAXIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
@Override protected double computeMaxHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
return super.computeMaxHeight(Math.min(MAXIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
@Override protected double computePrefWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
double prefHeight = PREFERRED_HEIGHT;
if (HEIGHT != -1) {
prefHeight = Math.max(0, HEIGHT - TOP_INSET - BOTTOM_INSET);
}
return super.computePrefWidth(prefHeight, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
@Override protected double computePrefHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
double prefWidth = PREFERRED_WIDTH;
if (WIDTH != -1) {
prefWidth = Math.max(0, WIDTH - LEFT_INSET - RIGHT_INSET);
}
return super.computePrefHeight(prefWidth, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
}
// ******************** Private Methods ***********************************
private void rotateNeedle() {
angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() - getSkinnable().getMinValue());
double targetAngle = needleRotate.getAngle() + (getSkinnable().getValue() - getSkinnable().getOldValue()) * angleStep;
targetAngle = clamp(180 - getSkinnable().getStartAngle(), 180 - getSkinnable().getStartAngle() + getSkinnable().getAngleRange(), targetAngle);
if (getSkinnable().isAnimated()) {
timeline.stop();
final KeyValue KEY_VALUE = new KeyValue(needleRotate.angleProperty(), targetAngle, Interpolator.SPLINE(0.5, 0.4, 0.4, 1.0));
final KeyFrame KEY_FRAME = new KeyFrame(Duration.millis(getSkinnable().getAnimationDuration()), KEY_VALUE);
timeline.getKeyFrames().setAll(KEY_FRAME);
timeline.play();
} else {
needleRotate.setAngle(targetAngle);
}
}
private final void drawSections() {
sectionsCtx.clearRect(0, 0, size, size);
final double MIN_VALUE = getSkinnable().getMinValue();
final double MAX_VALUE = getSkinnable().getMaxValue();
final double OFFSET = getSkinnable().getStartAngle() - 90;
final int NO_OF_SECTIONS = getSkinnable().getSections().size();
final double SECTIONS_OFFSET = size * 0.015;
final double SECTIONS_SIZE = size - (size * 0.03);
angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() + getSkinnable().getMinValue());
double sinValue;
double cosValue;
for (int i = 0 ; i < NO_OF_SECTIONS ; i++) {
final Section SECTION = getSkinnable().getSections().get(i);
final double SECTION_START_ANGLE;
if (SECTION.getStart() > MAX_VALUE || SECTION.getStop() < MIN_VALUE) continue;
if (SECTION.getStart() < MIN_VALUE && SECTION.getStop() < MAX_VALUE) {
SECTION_START_ANGLE = MIN_VALUE * angleStep;
} else {
SECTION_START_ANGLE = (SECTION.getStart() - MIN_VALUE) * angleStep;
}
final double SECTION_ANGLE_EXTEND;
if (SECTION.getStop() > MAX_VALUE) {
SECTION_ANGLE_EXTEND = MAX_VALUE * angleStep;
} else {
SECTION_ANGLE_EXTEND = (SECTION.getStop() - SECTION.getStart()) * angleStep;
}
sectionsCtx.save();
switch(i) {
case 0: sectionsCtx.setFill(getSkinnable().getSectionFill0()); break;
case 1: sectionsCtx.setFill(getSkinnable().getSectionFill1()); break;
case 2: sectionsCtx.setFill(getSkinnable().getSectionFill2()); break;
case 3: sectionsCtx.setFill(getSkinnable().getSectionFill3()); break;
case 4: sectionsCtx.setFill(getSkinnable().getSectionFill4()); break;
case 5: sectionsCtx.setFill(getSkinnable().getSectionFill5()); break;
case 6: sectionsCtx.setFill(getSkinnable().getSectionFill6()); break;
case 7: sectionsCtx.setFill(getSkinnable().getSectionFill7()); break;
case 8: sectionsCtx.setFill(getSkinnable().getSectionFill8()); break;
case 9: sectionsCtx.setFill(getSkinnable().getSectionFill9()); break;
}
sectionsCtx.fillArc(SECTIONS_OFFSET, SECTIONS_OFFSET, SECTIONS_SIZE, SECTIONS_SIZE, (OFFSET - SECTION_START_ANGLE), -SECTION_ANGLE_EXTEND, ArcType.ROUND);
// Draw Section Text
if (getSkinnable().isSectionTextVisible()) {
sinValue = -Math.sin(Math.toRadians(OFFSET - 90 - SECTION_START_ANGLE - SECTION_ANGLE_EXTEND * 0.5));
cosValue = -Math.cos(Math.toRadians(OFFSET - 90 - SECTION_START_ANGLE - SECTION_ANGLE_EXTEND * 0.5));
Point2D textPoint = new Point2D(size * 0.5 + size * 0.365 * sinValue, size * 0.5 + size * 0.365 * cosValue);
sectionsCtx.setFont(Font.font("Open Sans", FontWeight.NORMAL, 0.08 * size));
sectionsCtx.setTextAlign(TextAlignment.CENTER);
sectionsCtx.setTextBaseline(VPos.CENTER);
sectionsCtx.setFill(getSkinnable().getSectionTextColor());
sectionsCtx.fillText(SECTION.getText(), textPoint.getX(), textPoint.getY());
}
// Draw Section Icon
if (size > 0) {
if (getSkinnable().isSectionIconVisible() && !getSkinnable().isSectionTextVisible()) {
if (null != SECTION.getImage()) {
Image icon = SECTION.getImage();
sinValue = -Math.sin(Math.toRadians(OFFSET - 90 - SECTION_START_ANGLE - SECTION_ANGLE_EXTEND * 0.5));
cosValue = -Math.cos(Math.toRadians(OFFSET - 90 - SECTION_START_ANGLE - SECTION_ANGLE_EXTEND * 0.5));
Point2D iconPoint = new Point2D(size * 0.5 + size * 0.365 * sinValue, size * 0.5 + size * 0.365 * cosValue);
sectionsCtx.drawImage(icon, iconPoint.getX() - size * 0.06, iconPoint.getY() - size * 0.06, size * 0.12, size * 0.12);
}
}
}
sectionsCtx.restore();
// Draw white border around area
sectionsCtx.setStroke(Color.WHITE);
sectionsCtx.setLineWidth(size * 0.032);
sectionsCtx.strokeArc(SECTIONS_OFFSET, SECTIONS_OFFSET, SECTIONS_SIZE, SECTIONS_SIZE, OFFSET + 90, 270, ArcType.ROUND);
}
}
private final void drawMeasuredRange() {
final double MIN_VALUE = getSkinnable().getMinValue();
final double OFFSET = getSkinnable().getStartAngle() - 90;
final double START_ANGLE = (getSkinnable().getMinMeasuredValue() - MIN_VALUE) * angleStep;
final double ANGLE_EXTEND = (getSkinnable().getMaxMeasuredValue() - getSkinnable().getMinMeasuredValue()) * angleStep;
final double RANGE_OFFSET = size * 0.015;
final double RANGE_SIZE = size - (size * 0.03);
measuredRangeCtx.save();
measuredRangeCtx.clearRect(0, 0, size, size);
measuredRangeCtx.setFill(getSkinnable().getRangeFill());
measuredRangeCtx.fillArc(RANGE_OFFSET, RANGE_OFFSET, RANGE_SIZE, RANGE_SIZE, (OFFSET - START_ANGLE), -ANGLE_EXTEND, ArcType.ROUND);
measuredRangeCtx.setStroke(Color.WHITE);
measuredRangeCtx.setLineWidth(size * 0.032);
measuredRangeCtx.strokeArc(RANGE_OFFSET, RANGE_OFFSET, RANGE_SIZE, RANGE_SIZE, (OFFSET - START_ANGLE), -ANGLE_EXTEND, ArcType.ROUND);
measuredRangeCtx.restore();
}
private double clamp(final double MIN_VALUE, final double MAX_VALUE, final double VALUE) {
if (VALUE < MIN_VALUE) return MIN_VALUE;
if (VALUE > MAX_VALUE) return MAX_VALUE;
return VALUE;
}
private void resizeText() {
value.setFont(Font.font("Open Sans", FontWeight.BOLD, size * 0.145));
if (value.getLayoutBounds().getWidth() > 0.38 * size) {
double decrement = 0d;
while (value.getLayoutBounds().getWidth() > 0.38 * size && value.getFont().getSize() > 0) {
value.setFont(Font.font("Open Sans", FontWeight.BOLD, size * (0.15 - decrement)));
decrement += 0.01;
}
}
value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5);
value.setTranslateY(size * (title.getText().isEmpty() ? 0.5 : 0.48));
title.setFont(Font.font("Open Sans", FontWeight.BOLD, size * 0.045));
if (title.getLayoutBounds().getWidth() > 0.38 * size) {
double decrement = 0d;
while (title.getLayoutBounds().getWidth() > 0.38 * size && title.getFont().getSize() > 0) {
title.setFont(Font.font("Open Sans", FontWeight.BOLD, size * (0.05 - decrement)));
decrement += 0.01;
}
}
title.setTranslateX((size - title.getLayoutBounds().getWidth()) * 0.5);
title.setTranslateY(size * 0.5 + value.getFont().getSize() * 0.7);
}
private void resize() {
size = getSkinnable().getWidth() < getSkinnable().getHeight() ? getSkinnable().getWidth() : getSkinnable().getHeight();
sectionsCanvas.setWidth(size);
sectionsCanvas.setHeight(size);
drawSections();
sectionsCanvas.setCache(true);
sectionsCanvas.setCacheHint(CacheHint.QUALITY);
measuredRangeCanvas.setWidth(size);
measuredRangeCanvas.setHeight(size);
drawMeasuredRange();
double currentValue = (needleRotate.getAngle() + getSkinnable().getStartAngle() - 180) / angleStep + getSkinnable().getMinValue();
value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", currentValue) + getSkinnable().getUnit());
//value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", (needleRotate.getAngle() + getSkinnable().getStartAngle() - 180) / angleStep) + getSkinnable().getUnit());
title.setText(getSkinnable().getTitle());
needle.getElements().clear();
needle.getElements().add(new MoveTo(0.275 * size, 0.5 * size));
needle.getElements().add(new CubicCurveTo(0.275 * size, 0.62426575 * size,
0.37573425 * size, 0.725 * size,
0.5 * size, 0.725 * size));
needle.getElements().add(new CubicCurveTo(0.62426575 * size, 0.725 * size,
0.725 * size, 0.62426575 * size,
0.725 * size, 0.5 * size));
needle.getElements().add(new CubicCurveTo(0.725 * size, 0.3891265 * size,
0.6448105 * size, 0.296985 * size,
0.5392625 * size, 0.2784125 * size));
needle.getElements().add(new LineTo(0.5 * size, 0.0225));
needle.getElements().add(new LineTo(0.4607375 * size, 0.2784125 * size));
needle.getElements().add(new CubicCurveTo(0.3551895 * size, 0.296985 * size,
0.275 * size, 0.3891265 * size,
0.275 * size, 0.5 * size));
needle.getElements().add(new ClosePath());
needle.setStrokeWidth(size * 0.03);
needle.relocate(needle.getLayoutBounds().getMinX(), needle.getLayoutBounds().getMinY());
needleRotate.setPivotX(size * 0.5);
needleRotate.setPivotY(size * 0.5);
resizeText();
}
}