/**
* Copyright 2014 (C) Mr LoNee - (Laurent NICOLAS) - www.mrlonee.com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package com.mrlonee.radialfx.thermostat;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Group;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.BoxBlur;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.SVGPath;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import com.mrlonee.radialfx.core.RadialMenuItem;
import com.mrlonee.radialfx.core.RadialMenuItemBuilder;
/**
* Nest Thermostat reproduction in JavaFX.
*
* @author MrLoNee
*/
public class NestNoCss extends Region {
private Circle frame;
private Circle frame1;
private Circle frame2;
private Circle frame3;
private SVGPath line;
private Ellipse lightEffect;
private SVGPath line1;
private List<RadialMenuItem> arcs = new ArrayList<RadialMenuItem>();
private RadialMenuItem targetTemperatureTick;
private RadialMenuItem currentTemperatureTick;
private Text currentTemperatureText;
private Text reachTargetTemperatureDelayText;
private InvalidationListener listener = new InternalListener();
private final static double initialSize = 533.919;
private static final Paint FRAME_FILL = new LinearGradient(0.271, 0.065,
0.7735, 0.91, true, CycleMethod.NO_CYCLE, new Stop(0.0,
Color.web("#e8e8e8")), new Stop(0.5, Color.web("#c6c6c6")),
new Stop(1.0, Color.web("#a6a6a6")));
private static final Paint FRAME1_FILL = new LinearGradient(0.271, 0.065,
0.7735, 0.91, true, CycleMethod.NO_CYCLE, new Stop(0.0,
Color.web("#fdfdfd")), new Stop(0.5, Color.web("#747474")),
new Stop(1.0, Color.web("#a8a8a8")));
private static final Paint FRAME1_STROKE = new LinearGradient(0.271, 0.065,
0.7735, 0.91, true, CycleMethod.NO_CYCLE, new Stop(0.0,
Color.web("#d5d5d5")), new Stop(0.5, Color.web("#747474")),
new Stop(1.0, Color.web("#8f8f8f")));
private static final Paint FRAME2_FILL =new LinearGradient(0.271, 0.065,
0.7735, 0.91, true, CycleMethod.NO_CYCLE, new Stop(0.0,
Color.web("#1c1715")), new Stop(0.5, Color.web("#181818")),
new Stop(1.0, Color.web("#3a3a3a")));
private static final Paint FRAME2_STROKE = Color.web("#212121");
private double size;
private double frame1Ratio = 0.98;
private double frame2Ratio = 0.90;
private double frame3Ratio = 0.54;
private double tickRatio = 0.48;
private double tickRatioOffset = 0.1;
private double targetTickRatioOffset = 0.15;
private double temperatureTextYRatio = 0.5;
private double lightEffectXRatio = 0.2;
private double lightEffectYRatio = 0.13;
private double lightEffectXRadiusRatio = 0.3;
private double lightEffectYRadiusRatio = 0.17;
private double lightEffectRotate = -45;
private double nbArcs = 110;
private double arcEmptyLength = 45;
private double shadowXOffset = 0.1;
private double shadowYOffset = 0.2;
private double shadowSizeOffset = 0.25;
private Color CURRENT_TICK_COLOR = Color.web("#ffffffff");
private Color TARGET_TICK_COLOR = Color.web("#ffffffff");
private Color TICK_COLOR = Color.web("#ffffffa0");
private ObjectProperty<NumberFormat> numberFormatProperty;
private DoubleProperty currentTemperatureProperty;
private DoubleProperty targetTemperatureProperty;
private DoubleProperty maxTemperatureProperty;
private DoubleProperty minTemperatureProperty;
private IntegerProperty reachTargetTemperatureDelayProperty;
private DropShadow shadow;
private ChangeListener<? super Number> targetTemperatureTickListener;
public double getTargetTemperature() {
return targetTemperatureProperty.get();
}
public void setTargetTemperature(final double value) {
targetTemperatureProperty.set(value);
}
public DoubleProperty targetTemperatureProperty() {
return targetTemperatureProperty;
}
public double getReachTargetTemperatureDelay() {
return reachTargetTemperatureDelayProperty.get();
}
public void setReachTargetTemperatureDelay(final int value) {
reachTargetTemperatureDelayProperty.set(value);
}
public IntegerProperty reachTargetTemperatureDelay() {
return reachTargetTemperatureDelayProperty;
}
public double getCurrentTemperature() {
return currentTemperatureProperty.get();
}
public void setCurrentTemperature(final double value) {
currentTemperatureProperty.set(value);
}
public DoubleProperty currentTemperatureProperty() {
return currentTemperatureProperty;
}
public NumberFormat getNumberFormat() {
return numberFormatProperty.get();
}
public void setNumberFormat(final NumberFormat numberFormat) {
numberFormatProperty.set(numberFormat);
}
public ObjectProperty<NumberFormat> numberFormatProperty() {
return numberFormatProperty;
}
public NestNoCss() {
currentTemperatureProperty = new SimpleDoubleProperty(18);
targetTemperatureProperty = new SimpleDoubleProperty(24);
minTemperatureProperty = new SimpleDoubleProperty(0);
maxTemperatureProperty = new SimpleDoubleProperty(50);
reachTargetTemperatureDelayProperty = new SimpleIntegerProperty(0);
numberFormatProperty = new SimpleObjectProperty<NestNoCss.NumberFormat>(
NumberFormat.AUTO);
targetTemperatureTickListener = new ChangeListener<Number>() {
@Override
public void changed(final ObservableValue<? extends Number> obdVal,
final Number oldVal, final Number newVal) {
// for(final RadialMenuItem item : arcs){
// final double diff = Math.abs(item.getStartAngle() -
// newVal.doubleValue());
// if(diff <= 2){
// if(item.getLength() == 0.7){
// item.setLength(1.3);
// item.setStartAngle(item.getStartAngle()-0.1);
//
// }
// }
// }
}
};
// getStylesheets().add(
// getClass().getResource("nest.css").toExternalForm());
// getStyleClass().setAll("nest");
initGraphcis();
registerListeners();
}
private void initGraphcis() {
// frame = new Region();
// frame.getStyleClass().setAll("frame");
frame = new Circle();
frame.setFill(FRAME_FILL);
shadow = new DropShadow();
shadow.setBlurType(BlurType.ONE_PASS_BOX);
shadow.setColor(Color.rgb(0, 0, 0, 0.4));
frame.setEffect(shadow);
frame1 = new Circle();
// frame1.getStyleClass().setAll("frame1");
frame1.setFill(FRAME1_FILL);
frame1.setStroke(FRAME1_STROKE);
frame1.setStrokeWidth(2.0);
frame2 = new Circle();
// frame2.getStyleClass().setAll("frame2");
frame2.setFill(FRAME2_FILL);
frame2.setStroke(FRAME2_STROKE);
frame3 = new Circle();
// frame3.getStyleClass().setAll("frame3");
frame3.setFill(Color.web("#c44f1a"));
line = new SVGPath();
line.setContent("M 0.75,1.806272 C 0.75,1.806272 67.422114,-2.659598 118.06708,1.085452 130.59357,2.011752 166.81696,11.039202 185.35089,11.189052 206.02921,11.356252 242.24677,2.052122 255.84883,1.085452 304.58057,-2.377808 372.89963,1.806272 372.89963,1.806272");
// line.getStyleClass().setAll("line");
line.setFill(Color.web("#ffffff00"));
line.setStroke(Color.web("#4d4d4d"));
line.setStrokeWidth(1.5);
line1 = new SVGPath();
line1.setContent("M 0.75,1.806272 C 0.75,1.806272 67.422114,-2.659598 118.06708,1.085452 130.59357,2.011752 166.81696,11.039202 185.35089,11.189052 206.02921,11.356252 242.24677,2.052122 255.84883,1.085452 304.58057,-2.377808 372.89963,1.806272 372.89963,1.806272");
// line.getStyleClass().setAll("line1");
line.setFill(Color.web("#ffffff00"));
line.setStroke(Color.web("#141414"));
line.setStrokeWidth(1.5);
final double length = 0.7;
final Group arcContainer = new Group();
for (int i = 0; i < nbArcs; i++) {
final RadialMenuItem item = RadialMenuItemBuilder
.create()
.startAngle(
-length
/ 2.0
- arcEmptyLength
+ i
* ((360 - (arcEmptyLength * 2.0)) / (nbArcs - 1)))
.length(length).backgroundFill(TICK_COLOR)
.strokeVisible(false).build();
item.getStyleClass().setAll("ticks");
arcs.add(item);
arcContainer.getChildren().add(item);
}
currentTemperatureTick = RadialMenuItemBuilder.create().length(3.2)
.backgroundFill(CURRENT_TICK_COLOR).strokeVisible(false)
.clockwise(true).build();
final DoubleBinding currentTickStartAngleBinding = new DoubleBinding() {
{
super.bind(currentTemperatureProperty, maxTemperatureProperty,
minTemperatureProperty);
}
@Override
protected double computeValue() {
return -225
+ (1 - (maxTemperatureProperty.get() - currentTemperatureProperty
.get())
/ (maxTemperatureProperty.get() - minTemperatureProperty
.get())) * (360 - arcEmptyLength * 2.0)
- currentTemperatureTick.getLength() / 2.0;
}
};
currentTemperatureTick.startAngleProperty().bind(
currentTickStartAngleBinding);
targetTemperatureTick = RadialMenuItemBuilder.create().length(3)
.backgroundFill(TARGET_TICK_COLOR).strokeVisible(false)
.clockwise(true).build();
final DoubleBinding targetTickStartAngleBinding = new DoubleBinding() {
{
super.bind(targetTemperatureProperty, maxTemperatureProperty,
minTemperatureProperty);
}
@Override
protected double computeValue() {
return -225
+ (1 - (maxTemperatureProperty.get() - targetTemperatureProperty
.get())
/ (maxTemperatureProperty.get() - minTemperatureProperty
.get())) * (360 - arcEmptyLength * 2.0)
- targetTemperatureTick.getLength() / 2.0;
}
};
targetTemperatureTick.startAngleProperty().bind(
targetTickStartAngleBinding);
currentTemperatureText = new Text();
// currentTemperatureText.getStyleClass().setAll("text");
currentTemperatureText.setFill(Color.web("#ffffff"));
currentTemperatureText.setFont(Font.font(Font.getDefault().getName(), FontWeight.BOLD, 86));
final StringBinding textBinding = new StringBinding() {
{
super.bind(targetTemperatureProperty, numberFormatProperty);
}
@Override
protected String computeValue() {
return numberFormatProperty.get().format(
targetTemperatureProperty.getValue());
}
};
currentTemperatureText.textProperty().bind(textBinding);
reachTargetTemperatureDelayText = new Text();
// reachTargetTemperatureDelayText.getStyleClass().setAll("delaytext");
reachTargetTemperatureDelayText.setFill(Color.web("#ffffffff"));
reachTargetTemperatureDelayText.setFont(Font.font(Font.getDefault().getName(), FontWeight.LIGHT, 18));
final StringBinding delayTextBinding = new StringBinding() {
{
super.bind(reachTargetTemperatureDelayProperty);
}
@Override
protected String computeValue() {
return "IN " + reachTargetTemperatureDelayProperty.get()
+ " MIN";
}
};
reachTargetTemperatureDelayText.textProperty().bind(delayTextBinding);
lightEffect = new Ellipse();
lightEffect.setFill(Color.rgb(255, 255, 255, 0.7));
lightEffect.setEffect(new BoxBlur(90, 90, 5));
lightEffect.setCache(true);
getChildren().setAll(frame, frame1, frame2, frame3, line, line1, arcContainer,
currentTemperatureTick, targetTemperatureTick,
currentTemperatureText, reachTargetTemperatureDelayText);
}
private void registerListeners() {
widthProperty().addListener(listener);
heightProperty().addListener(listener);
currentTemperatureText.boundsInLocalProperty().addListener(listener);
reachTargetTemperatureDelayText.boundsInLocalProperty().addListener(
listener);
}
private void resize() {
size = getWidth() < getHeight() ? getWidth() : getHeight();
// frame.setPrefSize(size, size);
frame.setRadius(size / 2.0);
frame.setTranslateX(size / 2.0);
frame.setTranslateY(size / 2.0);
frame1.setRadius(frame1Ratio * size / 2.0);
frame1.setTranslateX(size / 2.0);
frame1.setTranslateY(size / 2.0);
shadow.setOffsetX(size * shadowXOffset);
shadow.setOffsetY(size * shadowYOffset);
shadow.setRadius(size * shadowSizeOffset);
shadow.setSpread(0.099);
frame2.setRadius(frame2Ratio * size / 2.0);
frame2.setTranslateX(size / 2.0);
frame2.setTranslateY(size / 2.0);
frame3.setRadius(frame3Ratio * size / 2.0);
frame3.setTranslateX(size / 2.0);
frame3.setTranslateY(size / 2.0);
final double scaleRatio = size / initialSize;
line1.setScaleX(scaleRatio);
line1.setScaleY(scaleRatio);
final double lineWidth = line1.getBoundsInLocal().getWidth();
line1.setTranslateX(size / 2.0 - lineWidth / 2.0);
line1.setTranslateY(size * 408.72054 / initialSize);
line.setScaleX(scaleRatio);
line.setScaleY(scaleRatio);
line.setTranslateX(size / 2.0 - lineWidth / 2.0);
line.setTranslateY(size * 410.08419 / initialSize);
for (final RadialMenuItem arc : arcs) {
arc.setTranslateX(size / 2.0);
arc.setTranslateY(size / 2.0);
arc.setRadius(tickRatio * size / 2.0);
arc.setInnerRadius((tickRatio - tickRatioOffset) * size / 2.0);
}
currentTemperatureTick.setTranslateX(size / 2.0);
currentTemperatureTick.setTranslateY(size / 2.0);
currentTemperatureTick.setRadius(tickRatio * size / 2.0);
currentTemperatureTick.setInnerRadius((tickRatio - tickRatioOffset)
* size / 2.0);
targetTemperatureTick.setTranslateX(size / 2.0);
targetTemperatureTick.setTranslateY(size / 2.0);
targetTemperatureTick.setRadius(tickRatio * size / 2.0);
targetTemperatureTick
.setInnerRadius((tickRatio - targetTickRatioOffset) * size
/ 2.0);
targetTemperatureTick.startAngleProperty().addListener(
targetTemperatureTickListener);
currentTemperatureText.setScaleX(scaleRatio);
currentTemperatureText.setScaleY(scaleRatio);
final double textWidth = currentTemperatureText.getBoundsInLocal()
.getWidth();
final double textHeight = currentTemperatureText.getBoundsInLocal()
.getHeight();
currentTemperatureText.setTranslateX(size / 2.0 - textWidth / 2.0);
currentTemperatureText.setTranslateY(temperatureTextYRatio * size
+ textHeight / 4.0);
reachTargetTemperatureDelayText.setScaleX(scaleRatio);
reachTargetTemperatureDelayText.setScaleY(scaleRatio);
final double text2Width = reachTargetTemperatureDelayText
.getBoundsInLocal().getWidth();
final double text2Height = reachTargetTemperatureDelayText
.getBoundsInLocal().getHeight();
reachTargetTemperatureDelayText.setTranslateX(size / 2.0 - text2Width
/ 2.0);
reachTargetTemperatureDelayText
.setTranslateY((temperatureTextYRatio - 0.1) * size
+ text2Height / 4.0);
lightEffect.setRotate(lightEffectRotate);
lightEffect.setTranslateX(lightEffectXRatio * size);
lightEffect.setTranslateY(lightEffectYRatio * size);
lightEffect.setRadiusX(lightEffectXRadiusRatio * size);
lightEffect.setRadiusY(lightEffectYRadiusRatio * size);
}
private final class InternalListener implements InvalidationListener {
@Override
public void invalidated(final Observable value) {
resize();
}
}
public static enum NumberFormat {
AUTO("0"), STANDARD("0"), FRACTIONAL("0.0#"), SCIENTIFIC("0.##E0"), PERCENTAGE(
"##0.0%");
private final DecimalFormat DF;
private NumberFormat(final String FORMAT_STRING) {
Locale.setDefault(new Locale("en", "US"));
DF = new DecimalFormat(FORMAT_STRING);
}
public String format(final Number NUMBER) {
return DF.format(NUMBER);
}
}
}