/* * 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(); } }