All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.internal.scene.control.gauge.linear.skin.BasicRoundDailGaugeSkin Maven / Gradle / Ivy

The newest version!
/**
 * BasicRoundDailGaugeSkin.java
 *
 * Copyright (c) 2011-2016, JFXtras
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the organization nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package jfxtras.internal.scene.control.gauge.linear.skin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.CacheHint;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
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.scene.transform.Scale;
import javafx.util.Duration;
import jfxtras.css.CssMetaDataForSkinProperty;
import jfxtras.scene.control.gauge.linear.BasicRoundDailGauge;
import jfxtras.scene.control.gauge.linear.elements.Label;
import jfxtras.scene.control.gauge.linear.elements.Marker;
import jfxtras.scene.control.gauge.linear.elements.Segment;

import javafx.css.converter.PaintConverter;

/**
 * 
 */
public class BasicRoundDailGaugeSkin extends AbstractLinearGaugeSkin {

	private static final double RING_OUTER_RADIUS_FACTOR = 0.99;
	private static final double RING_INNER_RADIUS_FACTOR = 0.96;
	private static final double RING_WIDTH_FACTOR = 0.04;
	private static final double BACKPLATE_RADIUS_FACTOR = 0.96;
	private static final double TICK_OUTER_RADIUS_FACTOR = 0.93;
	private static final double TICK_INNER_RADIUS_FACTOR = 0.85;
	private static final double TICK_MINOR_RADIUS_FACTOR = 0.82;
	private static final double TICK_MAJOR_RADIUS_FACTOR = 0.80;
	private static final double LABEL_RADIUS_FACTOR = 0.60;
	private static final double SEGMENT_INNER_RADIUS_FACTOR = TICK_MINOR_RADIUS_FACTOR;
	private static final double MARKER_RADIUS_FACTOR = TICK_MAJOR_RADIUS_FACTOR * 0.95;
	private static final double INDICATOR_RADIUS_FACTOR = 0.30;
	static final private double FULL_ARC_IN_DEGREES = 270.0;

	// ==================================================================================================================
	// CONSTRUCTOR
	
	/**
	 * 
	 */
	public BasicRoundDailGaugeSkin(BasicRoundDailGauge control) {
		super(control);
		constructNodes();
	}
	
	// ==================================================================================================================
	// StyleableProperties
	
    /**
     * tickcolor
     */
    public final ObjectProperty tickColorProperty() { return tickColorProperty; }
    private ObjectProperty tickColorProperty = new SimpleStyleableObjectProperty(StyleableProperties.TICKCOLOR_CSSMETADATA, StyleableProperties.TICKCOLOR_CSSMETADATA.getInitialValue(null));
    public final void setTickColor(Paint value) { tickColorProperty().set(value); }
    public final Paint getTickColor() { return tickColorProperty.get(); }
    public final BasicRoundDailGaugeSkin withTickColor(Paint value) { setTickColor(value); return this; }


    // -------------------------
        
    private static class StyleableProperties 
    {
        private static final CssMetaData TICKCOLOR_CSSMETADATA = new CssMetaDataForSkinProperty("-fxx-tick-color", PaintConverter.getInstance(), Color.BLACK ) {
        	@Override 
        	protected ObjectProperty getProperty(BasicRoundDailGaugeSkin s) {
            	return s.tickColorProperty;
            }
        };
        
        private static final List> STYLEABLES;
        static  {
            final List> styleables = new ArrayList>(SkinBase.getClassCssMetaData());
            styleables.add(TICKCOLOR_CSSMETADATA);
            STYLEABLES = Collections.unmodifiableList(styleables);                
        }
    }
    
    /** 
     * @return The CssMetaData associated with this class, which may include the
     * CssMetaData of its super classes.
     */    
    public static List> getClassCssMetaData() {
    	List> classCssMetaData = AbstractLinearGaugeSkin.getClassCssMetaData();
    	classCssMetaData = new ArrayList>(classCssMetaData);
    	classCssMetaData.addAll(StyleableProperties.STYLEABLES);
    	return Collections.unmodifiableList(classCssMetaData);
    }

    /**
     * This method should delegate to {@link Node#getClassCssMetaData()} so that
     * a Node's CssMetaData can be accessed without the need for reflection.
     * @return The CssMetaData associated with this node, which may include the
     * CssMetaData of its super classes.
     */
    public List> getCssMetaData() {
        return getClassCssMetaData();
    }
        

	// ==================================================================================================================
	// DRAW
	
	/**
	 * construct the nodes
	 */
	private void constructNodes()
	{
		// style
		getSkinnable().getStyleClass().add(getClass().getSimpleName()); // always add self as style class, because with multiple skins CSS should relate to the skin not the control		

		// determine center
		centerX.bind(stackPane.widthProperty().multiply(0.5));
		centerY.bind(stackPane.heightProperty().multiply(0.5));

		// use a stack pane to control the layers
		stackPane.getChildren().addAll(segmentPane, backPlatePane, markerPane, indicatorPane, valuePane, needlePane, glassPlatePane);
		getChildren().add(stackPane);
		stackPane.setPrefSize(200, 200);
	}
	final private SimpleDoubleProperty centerX = new SimpleDoubleProperty();
	final private SimpleDoubleProperty centerY = new SimpleDoubleProperty();
	final private SimpleDoubleProperty radius = new SimpleDoubleProperty();
	final private StackPane stackPane = new StackPane();
	final private SegmentPane segmentPane = new SegmentPane();
	final private BackPlatePane backPlatePane = new BackPlatePane();
	final private MarkerPane markerPane = new MarkerPane();
	final private IndicatorPane indicatorPane = new IndicatorPane();
	final private NeedlePane needlePane = new NeedlePane();
	final private ValuePane valuePane = new ValuePane();
	final private GlassPlatePane glassPlatePane = new GlassPlatePane();
	

	// ==================================================================================================================
	// Segment
	
	private class SegmentPane extends Pane {
		final private Map segmentToArc = new HashMap<>();

		/**
		 * 
		 */
		private SegmentPane() {
			this.getStyleClass().add("SegmentPane");
			
			// backpane
			backpaneCircle.getStyleClass().addAll("backplate");
			backpaneCircle.centerXProperty().bind(centerX);
			backpaneCircle.centerYProperty().bind(centerY);
            
			// react to changes in the segments
			getSkinnable().segments().addListener( (ListChangeListener.Change change) -> {
				createAndAddSegments();
			});
			createAndAddSegments();
		}
		
		/**
		 * 
		 */
		private void createAndAddSegments() {
	 		// determine what segments to draw
			getChildren().clear();
			getChildren().addAll(backpaneCircle);

	 		// create the nodes representing each segment
	 		segmentToArc.clear();
	 		int segmentCnt = 0;
	 		for (Segment segment : getSkinnable().segments()) {
	 			
	 			// create an arc for this segment
	 			Arc arc = new Arc();
				getChildren().add(arc);
				segmentToArc.put(segment, arc);
				
				// setup CSS on the path
		        arc.getStyleClass().addAll("segment", "segment" + segmentCnt);
		        if (segment.getId() != null) {
		        	arc.setId(segment.getId());
		        }
	 			segmentCnt++;
	 		}
		}
		final private Circle backpaneCircle = new Circle();
		
		/**
		 * 
		 */
		@Override
		protected void layoutChildren() {
			super.layoutChildren();

			// prep
			double radius = calculateRadius(); // radius must be calculated and cannot use bind

			// size the circle
			double plateRadius = radius * BACKPLATE_RADIUS_FACTOR;
			backpaneCircle.setRadius(plateRadius);
			
			// preparation
	 		double controlMinValue = getSkinnable().getMinValue();
	 		double controlMaxValue = getSkinnable().getMaxValue();
	 		double controlValueRange = controlMaxValue - controlMinValue;
	 		
			// layout the segments
	 		double segmentRadius = calculateRadius() * BACKPLATE_RADIUS_FACTOR;
	 		for (Segment segment : getSkinnable().segments()) {
	 			String message = validateSegment(segment);
	 			if (message != null) {
	 				new Throwable(message).printStackTrace();
	 				continue;
	 			}
	 			
	 			// layout the arc for this segment
	 	 		double segmentMinValue = segment.getMinValue();
	 	 		double segmentMaxValue = segment.getMaxValue();
	 			double startAngle = (segmentMinValue - controlMinValue) / controlValueRange * FULL_ARC_IN_DEGREES; 
	 			double endAngle = (segmentMaxValue - controlMinValue) / controlValueRange * FULL_ARC_IN_DEGREES; 
	 			Arc arc = segmentToArc.get(segment);
	 			if (arc != null) {
		 			arc.setCenterX(centerX.get());
		 			arc.setCenterY(centerY.get());
		 			arc.setRadiusX(segmentRadius);
		 			arc.setRadiusY(segmentRadius);
		 			// 0 degrees is on the right side of the circle (3 o'clock), the gauge starts in the bottom left (about 7 o'clock), so add 90 + 45 degrees to offset to that.
		 			// The arc draws counter clockwise, so we need to negate to make it clock wise.
		 			arc.setStartAngle(-1 * (startAngle + 135.0));
		 			arc.setLength(-1 * (endAngle - startAngle));
		 			arc.setType(ArcType.ROUND);
	 			}
	 		}
		}
	}
	
	// ==================================================================================================================
	// BackPlate
	
	private class BackPlatePane extends Pane {

		/**
		 * 
		 */
		private BackPlatePane() {
			this.getStyleClass().add("BackPlatePane");
			
			// backpane
			backpaneCircle.getStyleClass().addAll("backplate");
			backpaneCircle.setStyle("-fx-fill: -fxx-backplate-color;");
			backpaneCircle.centerXProperty().bind(centerX);
			backpaneCircle.centerYProperty().bind(centerY);
			
			// ticks
            ticksCanvas.setCache(true);
            ticksCanvas.setCacheHint(CacheHint.QUALITY);
            ticksCanvas.getStyleClass().addAll("tick");
			ticksCanvas.layoutXProperty().set(0.0);
			ticksCanvas.layoutYProperty().set(0.0);
			ticksCanvas.widthProperty().bind(stackPane.widthProperty());
			ticksCanvas.heightProperty().bind(stackPane.heightProperty());
			backpaneCircle.fillProperty().addListener( (observable) -> {
				layoutChildren();
			});
            
            // add them
			getChildren().addAll(backpaneCircle, ticksCanvas);
			
			// react to changes in the segments
			getSkinnable().segments().addListener( (ListChangeListener.Change change) -> {
				requestLayout();
			});
		}
		final private Circle backpaneCircle = new Circle();
		final private Canvas ticksCanvas = new Canvas();
		final private Text tickText = new Text();
		
		/**
		 * 
		 */
		@Override
		protected void layoutChildren() {
			super.layoutChildren();

			// prep
			double radius = calculateRadius(); // radius must be calculated and cannot use bind

			// size the circle
			double plateRadius = radius * (getSkinnable().segments().size() == 0 ? 0.99 : SEGMENT_INNER_RADIUS_FACTOR);
			backpaneCircle.setRadius(plateRadius);
			
			// paint the ticks
			double size = radius * 2.0;
			GraphicsContext graphicsContext = ticksCanvas.getGraphicsContext2D();
			graphicsContext.clearRect(0.0, 0.0, ticksCanvas.getWidth(), ticksCanvas.getHeight());
			graphicsContext.setStroke(getTickColor());
			double tickInnerRadius = radius * TICK_INNER_RADIUS_FACTOR;
			double tickOuterRadius = radius * TICK_OUTER_RADIUS_FACTOR;
			double tickMajorRadius = radius * TICK_MAJOR_RADIUS_FACTOR;
			double tickMinorRadius = radius * TICK_MINOR_RADIUS_FACTOR;
            for (int i = 0; i <= 100; i++) { 
            	double angle = FULL_ARC_IN_DEGREES / 100.0 * (double)i; 
            	Point2D outerPoint2D = calculatePointOnCircle(tickOuterRadius, angle);
            	Point2D innerPoint2D = null;
            	
            	// major
            	if (i % 10 == 0) {
                	innerPoint2D = calculatePointOnCircle(tickMajorRadius, angle);
	            	graphicsContext.setLineWidth(size * 0.0055);
            	}
            	// medium
            	else if (i % 5 == 0) {
                	innerPoint2D = calculatePointOnCircle(tickMinorRadius, angle);
	            	graphicsContext.setLineWidth(size * 0.0035);
            	}
            	// minor
            	else {
                	innerPoint2D = calculatePointOnCircle(tickInnerRadius, angle);
	            	graphicsContext.setLineWidth(size * 0.00225);
            	}
            	graphicsContext.strokeLine(innerPoint2D.getX(), innerPoint2D.getY(), outerPoint2D.getX(), outerPoint2D.getY());
            }
            for (Label lLabel : getSkinnable().labels()) {
            	double angle = FULL_ARC_IN_DEGREES / 100.0 * lLabel.getValue();            	
    			tickText.setFont(Font.font("Verdana", FontWeight.NORMAL, 0.045 * size));
        		 
                // Draw text
            	Point2D textPoint2D = calculatePointOnCircle(radius * LABEL_RADIUS_FACTOR, angle);
                graphicsContext.save();
                graphicsContext.translate(textPoint2D.getX(), textPoint2D.getY());
                graphicsContext.setFont(Font.font("Verdana", FontWeight.NORMAL, 0.045 * size));
                graphicsContext.setTextAlign(TextAlignment.CENTER);
                graphicsContext.setTextBaseline(VPos.CENTER);
                graphicsContext.setFill(getTickColor());
                graphicsContext.fillText(lLabel.getText(), 0, 0); // TBEERNOT print value and format
                graphicsContext.restore();
            }
		}
	}
	

	// ==================================================================================================================
	// Marker
	
	private class MarkerPane extends AbstractMarkerPane {

		@Override
		protected void positionAndScaleMarker(Marker marker, Rotate rotate, Scale scale) {
			
			// preparation
	 		double controlMinValue = getSkinnable().getMinValue();
	 		double controlMaxValue = getSkinnable().getMaxValue();
	 		double controlValueRange = controlMaxValue - controlMinValue;
	 		double radius = calculateRadius();
			double markerRadius = radius * MARKER_RADIUS_FACTOR;
	 		
 			// layout the svg shape 
 	 		double markerValue = marker.getValue();
 			double angle = (markerValue - controlMinValue) / controlValueRange * FULL_ARC_IN_DEGREES;
 			Region region = markerToRegion.get(marker);
 			Point2D markerPoint2D = calculatePointOnCircle(markerRadius, angle);
 			region.setLayoutX(markerPoint2D.getX());
 			region.setLayoutY(markerPoint2D.getY());
			rotate.setAngle(angle + 45.0); // the angle also determines the rotation	 			
 			scale.setX(2 * radius / 300.0); // SVG shape was created against a sample gauge with 300x300 pixels  
 			scale.setY(scale.getX()); 
		}
	}
	
	
	// ==================================================================================================================
	// Indicators
	
	protected class IndicatorPane extends AbstractIndicatorPane {
		
		@Override
		protected double calculateScaleFactor() {
			// SVG is setup on a virtual 100x100 canvas, it is scaled to fit the size of the gauge. For a width of 300 (radius 150) this is 30 pixels
			return 30.0/100.0 * BasicRoundDailGaugeSkin.this.calculateRadius()/150.0;
		}
		
		@Override
		protected Point2D calculateLocation(int idx) {

			// prepare
	 		double radius = calculateRadius();
			double indicatorRadius = radius * INDICATOR_RADIUS_FACTOR;
	 			
			// six positions
			if (idx < 6) {
				return calculatePointOnCircle(indicatorRadius, idx * FULL_ARC_IN_DEGREES / 5);
 			}
			System.err.println("The " + getSkinnable().getClass().getSimpleName() + " gauge supports indicators [0,4], not " + idx);
			return null;
		}
	}
	
	
	// ==================================================================================================================
	// VALUE

	private class ValuePane extends AbstractValuePane {

		/**
		 * 
		 */
		private ValuePane() {

			// position valueTextPane
			valueTextPane.layoutXProperty().bind(centerX.subtract( valueTextPane.widthProperty().multiply(0.5).multiply(valueScale.xProperty()) )); 
			valueTextPane.layoutYProperty().bind(centerY
					.add( radius.multiply(0.65) )
					.subtract( valueTextPane.heightProperty().multiply(0.5).multiply(valueScale.yProperty() )) 
					);
		}

		/**
		 * The value should automatically fill the needle as much as possible.
		 * But it should not constantly switch font size, so it cannot be based on the current content of value's Text node.
		 * So to determine how much the Text node must be scaled, the calculation is based on value's extremes: min and max value.
		 * The smallest scale factor is the one to use (using the larger would make the other extreme go out of the circle).   
		 */
		@Override
		protected void scaleValueText() {
			
			// preparation
	 		double radius = calculateRadius() * 0.70;
			
			// use the two extreme's to determine the scaling factor
			double minScale = calculateValueTextScaleFactor(radius, getSkinnable().getMinValue());
			double maxScale = calculateValueTextScaleFactor(radius, getSkinnable().getMaxValue());
			double scale = Math.min(minScale, maxScale);
			valueScale.setX(scale);
			valueScale.setY(scale);
		}
		
		
		/**
		 * Determine how much to scale the Text node containing the value to fill up the needle's circle
		 * @param radius The radius of the needle
		 * @param value The value to be rendered
		 * @return
		 */
		protected double calculateValueTextScaleFactor(double radius, double value) {
			hiddenText.setText(valueFormat(value));
			double width = hiddenText.getBoundsInParent().getWidth();
			double height = hiddenText.getBoundsInParent().getHeight();
			// Width and height construct a right angled triangle, where the hypotenuse should be equal to the available room
			// So apply some Pythagoras...
			//System.out.println(Math.sqrt((stackPane.getWidth()*stackPane.getWidth()) + (stackPane.getHeight()*stackPane.getHeight())));
			//System.out.println(Math.sqrt((width*width) + (height*height)));
			double scaleFactor = radius / Math.sqrt((width*width) + (height*height));
			return scaleFactor;
		}
	}

	// ==================================================================================================================
	// Needle
	
	private class NeedlePane extends Pane {

		/**
		 * 
		 */
		private NeedlePane() {
			this.getStyleClass().add("NeedlePane");
			
			// needle
			needleRegion.setPickOnBounds(false);
			needleRegion.getStyleClass().setAll("needle", "needle-standard");
			needleRegion.setPrefSize(6.0, 75.0);
			needleRegion.layoutXProperty().bind(centerX.add( needleRegion.widthProperty().multiply(-0.5) ));
			needleRegion.layoutYProperty().bind(centerY); //.add( needleRegion.heightProperty().multiply(-1.0) ));
			needleRotate.pivotXProperty().bind(needleRegion.widthProperty().multiply(0.5));
			needleRegion.getTransforms().add(needleRotate);
			needleRegion.getTransforms().add(needleScale);
			needleScale.yProperty().bind(needleScale.xProperty());

			// knob
			knobRegion.setPickOnBounds(false);
			knobRegion.getStyleClass().setAll("knob");
			knobRegion.layoutXProperty().bind(centerX);
			knobRegion.layoutYProperty().bind(centerY);
			knobRegion.getTransforms().add(new Scale(1.0, 1.0));

            // add them
			getChildren().addAll(needleRegion, knobRegion);
			
			getSkinnable().valueProperty().addListener( (observable) -> {
				if (!validateValueAndHandleInvalid()) {
					return;
				}
				rotateNeedle(true);
			});
			
	        // min and max value text need to be added to the scene in order to have the CSS applied
			getSkinnable().minValueProperty().addListener( (observable) -> {
				if (!validateValueAndHandleInvalid()) {
					return;
				}
				rotateNeedle(true); 
			});
			getSkinnable().maxValueProperty().addListener( (observable) -> {
				if (!validateValueAndHandleInvalid()) {
					return;
				}
				rotateNeedle(true);
			});
			rotateNeedle(false);
		}
		final private Region needleRegion = new Region();
		final private Rotate needleRotate = new Rotate();
		final private Scale needleScale = new Scale();
		final private Region knobRegion = new Region();
		
		/**
		 * 
		 */
		@Override
		protected void layoutChildren() {
			super.layoutChildren();

			// prep
			double radius = calculateRadius();
			
			// needle
			{
				needleScale.setX(radius / 100.0);
			}
			
			// knob
			{
				Scale scale = (Scale)knobRegion.getTransforms().get(0);
				scale.setX(radius / 200.0 * 0.3);
				scale.setY(scale.getX());
			}
		}
		
		/**
		 * @param allowAnimation AllowAnimation is needed only in the first pass during skin construction: the Animated property has not been set at that time, so we do not need if animation is wanted. So the initial rotation is always done unanimated.  
		 */
		private void rotateNeedle(boolean allowAnimation) {
			if (!validateValueAndHandleInvalid()) {
				return;
			}

			// preparation
	 		double controlMinValue = getSkinnable().getMinValue();
	 		double controlMaxValue = getSkinnable().getMaxValue();
	 		double controlValueRange = controlMaxValue - controlMinValue;
	 		double value = getSkinnable().getValue();
	 		double angle = (value - controlMinValue) / controlValueRange * FULL_ARC_IN_DEGREES;
	 		angle += 45;
	 		
	 		// We cannot use node.setRotate(angle), because this rotates always around the center of the node and the needle's rotation center is not the same as the node's center.
	 		// So we need to use the Rotate transformation, which allows to specify the center of rotation.
	 		// This however also means that we cannot use RotateTransition, because that manipulates the rotate property of a node (and -as explain above- we couldn't use that).
	 		// The only way to animate a Rotate transformation is to use a timeline and keyframes.
	 		if (allowAnimation == false || Animated.NO.equals(getAnimated())) {
	 	 		needleRotate.setAngle(angle);
	 		}
	 		else {
	 			timeline.stop();
		        final KeyValue KEY_VALUE = new KeyValue(needleRotate.angleProperty(), angle, Interpolator.SPLINE(0.5, 0.4, 0.4, 1.0));
		        final KeyFrame KEY_FRAME = new KeyFrame(Duration.millis(1000), KEY_VALUE);
		        timeline.getKeyFrames().setAll(KEY_FRAME);
		        timeline.play();
	 		}
	 		
	 		// make certain segments active because the needle moved
	 		activateSegments(segmentPane.segmentToArc);
		}
		final private Timeline timeline = new Timeline();
	}
	
	protected boolean validateValueAndHandleInvalid() {
		String validationMessage = validateValue();
		if (validationMessage != null) {
			new Throwable(validationMessage).printStackTrace();
			if (needlePane != null && valuePane != null) {
				valuePane.valueText.setText("");
				needlePane.needleRotate.setAngle(0.0);
			}
			return false;
		};
		return true;
	}


	// ==================================================================================================================
	// GlassPlate
	
	private class GlassPlatePane extends Pane {

		/**
		 * 
		 */
		private GlassPlatePane() {
			this.getStyleClass().add("GlassPlatePane");
			
			// backpane
			outerringCircle.getStyleClass().addAll("outerring");
			outerringCircle.centerXProperty().bind(centerX);
			outerringCircle.centerYProperty().bind(centerY);
			
			// backpane
			innerringCircle.getStyleClass().addAll("innerring");
			innerringCircle.centerXProperty().bind(centerX);
			innerringCircle.centerYProperty().bind(centerY);
			
            // add them
			getChildren().addAll(outerringCircle, innerringCircle);

			// clip the dropshadow
			clipCircle.centerXProperty().bind(centerX);
			clipCircle.centerYProperty().bind(centerY);
			clipCircle.setRadius(100.0); // just a dummy initial value
		    setClip(clipCircle);
		}
		final private Circle outerringCircle = new Circle();
		final private Circle innerringCircle = new Circle();
		final private Circle clipCircle = new Circle();
		
		/**
		 * 
		 */
		@Override
		protected void layoutChildren() {
			super.layoutChildren();

			// prep
			double radius = calculateRadius();
			
			// size the circle
			outerringCircle.setRadius(radius * RING_OUTER_RADIUS_FACTOR);
			outerringCircle.setStyle("-fx-stroke-width: " + (radius * RING_WIDTH_FACTOR) + ";");
			innerringCircle.setRadius(radius * RING_INNER_RADIUS_FACTOR);
			innerringCircle.setStyle("-fx-stroke-width: " + (radius * RING_WIDTH_FACTOR) + ";");
			if (outerringCircle.getRadius() > 1.0) {
				clipCircle.setRadius(this.getWidth() / 2);
			}
		}
	}
	
	// ==================================================================================================================
	// SUPPORT
	
	/**
	 * http://www.mathopenref.com/coordparamcircle.html
	 * @param center
	 * @param radius
	 * @param angleInDegrees
	 * @return
	 */
	private Point2D calculatePointOnCircle(double radius, double angleInDegrees) {
		// Java's math uses radians
		// 0 degrees is on the right side of the circle (3 o'clock), the gauge starts in the bottom left (about 7 o'clock), so add 90 + 45 degrees to offset to that. 
		double angleInRadians = Math.toRadians(angleInDegrees + 135.0);
		
		// calculate point on circle
		double x = centerX.get() + (radius * Math.cos(angleInRadians));
		double y = centerY.get() + (radius * Math.sin(angleInRadians));
		return new Point2D(x, y);
	}
	
	/**
	 * 
	 * @return
	 */
	private double calculateRadius() {
		radius.set( Math.min(centerX.get(), centerY.get()) );
		return radius.get();
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy