commit cf47f08d5e310f25d424845e675061a30f32a190 Author: clfreville2 Date: Thu Dec 15 10:26:04 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a835580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +### IntelliJ IDEA ### +.idea +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/Ebullition.iml b/Ebullition.iml new file mode 100644 index 0000000..08da7bc --- /dev/null +++ b/Ebullition.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/images/visualizer/cold.jpg b/resources/images/visualizer/cold.jpg new file mode 100644 index 0000000..0d18a4b Binary files /dev/null and b/resources/images/visualizer/cold.jpg differ diff --git a/resources/images/visualizer/hot.jpg b/resources/images/visualizer/hot.jpg new file mode 100644 index 0000000..91a82a4 Binary files /dev/null and b/resources/images/visualizer/hot.jpg differ diff --git a/resources/images/visualizer/warm.jpg b/resources/images/visualizer/warm.jpg new file mode 100644 index 0000000..09ba2a1 Binary files /dev/null and b/resources/images/visualizer/warm.jpg differ diff --git a/resources/windows/MainWindows.fxml b/resources/windows/MainWindows.fxml new file mode 100644 index 0000000..155f8fb --- /dev/null +++ b/resources/windows/MainWindows.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/fr/uca/iut/clfreville2/MainApplication.java b/src/fr/uca/iut/clfreville2/MainApplication.java new file mode 100644 index 0000000..ea3323e --- /dev/null +++ b/src/fr/uca/iut/clfreville2/MainApplication.java @@ -0,0 +1,18 @@ +package fr.uca.iut.clfreville2; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class MainApplication extends Application { + + @Override + public void start(Stage primaryStage) throws Exception { + Parent root = FXMLLoader.load(MainApplication.class.getResource("/windows/MainWindows.fxml")); + Scene scene = new Scene(root); + primaryStage.setScene(scene); + primaryStage.show(); + } +} diff --git a/src/fr/uca/iut/clfreville2/gui/MainWindows.java b/src/fr/uca/iut/clfreville2/gui/MainWindows.java new file mode 100644 index 0000000..fa0fa5d --- /dev/null +++ b/src/fr/uca/iut/clfreville2/gui/MainWindows.java @@ -0,0 +1,121 @@ +package fr.uca.iut.clfreville2.gui; + +import fr.uca.iut.clfreville2.gui.image.ImageSupplier; +import fr.uca.iut.clfreville2.gui.image.StandardImageSupplier; +import fr.uca.iut.clfreville2.gui.list.NameableListCell; +import fr.uca.iut.clfreville2.model.SensorRegistry; +import fr.uca.iut.clfreville2.model.binding.ToBooleanBinding; +import fr.uca.iut.clfreville2.model.sensor.ManualSensor; +import fr.uca.iut.clfreville2.model.sensor.Sensor; +import fr.uca.iut.clfreville2.persistence.StubSensorRegistryLoader; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.control.Slider; +import javafx.scene.control.TextField; +import javafx.scene.image.ImageView; +import javafx.scene.text.Text; + +public class MainWindows { + + private final SensorRegistry registry = new StubSensorRegistryLoader().load(); + private final ImageSupplier imageSupplier = new StandardImageSupplier(); + private ModalFactory modalFactory; + + @FXML + private ListView sensorsList; + + @FXML + private Text sensorId; + + @FXML + public TextField sensorName; + + @FXML + private Button changeBtn; + + @FXML + private Button visualizeBtn; + + @FXML + public void onChangeClick() { + Sensor selected = getSelectedSensor(); + if (!(selected instanceof ManualSensor sensor)) { + return; + } + getModalFactory().createModal(selected, root -> { + Slider slider = new Slider(-5, 30, sensor.getTemperature()); + slider.setBlockIncrement(0.5); + slider.setMajorTickUnit(0.5); + slider.setMinorTickCount(0); + slider.setSnapToTicks(true); + slider.valueProperty().bindBidirectional(sensor.temperatureProperty()); + root.getChildren().add(slider); + }).show(); + } + + @FXML + public void onVisualizeClick() { + Sensor selected = getSelectedSensor(); + if (selected == null) { + return; + } + getModalFactory().createModal(selected, root -> { + ImageView imageView = new ImageView(); + imageView.setPreserveRatio(true); + imageView.setFitHeight(200); + selected.temperatureProperty().addListener((observable, old, newValue) -> imageView.setImage(imageSupplier.apply(selected))); + imageView.setImage(imageSupplier.apply(selected)); + root.getChildren().add(imageView); + }).show(); + } + + @FXML + private void initialize() { + bindSensorList(); + bindActiveButtons(); + } + + @FXML + private void bindSensorList() { + sensorsList.setCellFactory(list -> new NameableListCell<>()); + sensorsList.itemsProperty().bind(registry.sensorsProperty()); + sensorsList.getSelectionModel().selectedItemProperty().addListener((list, oldValue, newValue) -> { + if (oldValue != null) { + sensorName.textProperty().unbindBidirectional(oldValue.nameProperty()); + } + if (newValue != null) { + sensorId.textProperty().bind(newValue.displayNameExpression()); + sensorName.textProperty().bindBidirectional(newValue.nameProperty()); + } else { + sensorId.textProperty().unbind(); + sensorId.setText(null); + } + }); + } + + @FXML + private void bindActiveButtons() { + changeBtn.visibleProperty().bind(new ToBooleanBinding<>( + sensorsList.getSelectionModel().selectedItemProperty(), + treeItem -> treeItem instanceof ManualSensor + )); + visualizeBtn.visibleProperty().bind(sensorsList.getSelectionModel().selectedItemProperty().isNotNull()); + } + + private Sensor getSelectedSensor() { + return sensorsList.getSelectionModel().getSelectedItem(); + } + + private ModalFactory getModalFactory() { + if (modalFactory == null) { + Scene scene = sensorsList.getScene(); + if (scene == null) { + throw new IllegalStateException("No scene found"); + } + modalFactory = new ModalFactory(scene.getWindow()); + } + return modalFactory; + } +} diff --git a/src/fr/uca/iut/clfreville2/gui/ModalFactory.java b/src/fr/uca/iut/clfreville2/gui/ModalFactory.java new file mode 100644 index 0000000..af1c7e1 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/gui/ModalFactory.java @@ -0,0 +1,44 @@ +package fr.uca.iut.clfreville2.gui; + +import fr.uca.iut.clfreville2.model.sensor.Sensor; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import javafx.stage.Window; + +import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; + +public class ModalFactory { + + private final Window owner; + + public ModalFactory(Window owner) { + this.owner = requireNonNull(owner, "window owner"); + } + + public Stage createModal(Consumer initializer) { + Stage stage = new Stage(); + Button quit = new Button("Quit"); + Pane root = new VBox(); + initializer.accept(root); + root.getChildren().add(quit); + stage.setScene(new Scene(root)); + stage.initOwner(owner); + quit.setOnAction((ev) -> stage.hide()); + return stage; + } + + public Stage createModal(Sensor sensor, Consumer initializer) { + return createModal(root -> { + Text name = new Text(); + name.textProperty().bind(sensor.displayNameExpression().concat(sensor.temperatureProperty().asString(" (%s°C)"))); + root.getChildren().add(name); + initializer.accept(root); + }); + } +} diff --git a/src/fr/uca/iut/clfreville2/gui/image/ImageSupplier.java b/src/fr/uca/iut/clfreville2/gui/image/ImageSupplier.java new file mode 100644 index 0000000..396a606 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/gui/image/ImageSupplier.java @@ -0,0 +1,12 @@ +package fr.uca.iut.clfreville2.gui.image; + +import fr.uca.iut.clfreville2.model.sensor.Sensor; +import javafx.scene.image.Image; + +import java.util.function.Function; + +/** + * Determine the image to display for a given sensor. + */ +public interface ImageSupplier extends Function { +} diff --git a/src/fr/uca/iut/clfreville2/gui/image/StandardImageSupplier.java b/src/fr/uca/iut/clfreville2/gui/image/StandardImageSupplier.java new file mode 100644 index 0000000..1b6f2ae --- /dev/null +++ b/src/fr/uca/iut/clfreville2/gui/image/StandardImageSupplier.java @@ -0,0 +1,30 @@ +package fr.uca.iut.clfreville2.gui.image; + +import fr.uca.iut.clfreville2.model.sensor.Sensor; +import javafx.scene.image.Image; + +public class StandardImageSupplier implements ImageSupplier { + private static final int HOT_THRESHOLD = 22; + private static final int WARM_THRESHOLD = 10; + + private final Image hotImage; + private final Image warmImage; + private final Image coldImage; + + public StandardImageSupplier() { + hotImage = new Image("/images/visualizer/hot.jpg"); + warmImage = new Image("/images/visualizer/warm.jpg"); + coldImage = new Image("/images/visualizer/cold.jpg"); + } + + @Override + public Image apply(Sensor sensor) { + if (sensor.getTemperature() >= HOT_THRESHOLD) { + return hotImage; + } + if (sensor.getTemperature() >= WARM_THRESHOLD) { + return warmImage; + } + return coldImage; + } +} diff --git a/src/fr/uca/iut/clfreville2/gui/list/NameableListCell.java b/src/fr/uca/iut/clfreville2/gui/list/NameableListCell.java new file mode 100644 index 0000000..4b6fedb --- /dev/null +++ b/src/fr/uca/iut/clfreville2/gui/list/NameableListCell.java @@ -0,0 +1,17 @@ +package fr.uca.iut.clfreville2.gui.list; + +import fr.uca.iut.clfreville2.model.shared.ObservableIdentifiable; +import javafx.scene.control.ListCell; + +public class NameableListCell extends ListCell { + + @Override + protected void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + textProperty().unbind(); + } else { + textProperty().bind(item.displayNameExpression()); + } + } +} diff --git a/src/fr/uca/iut/clfreville2/model/SensorRegistry.java b/src/fr/uca/iut/clfreville2/model/SensorRegistry.java new file mode 100644 index 0000000..21d8387 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/SensorRegistry.java @@ -0,0 +1,67 @@ +package fr.uca.iut.clfreville2.model; + +import fr.uca.iut.clfreville2.model.sensor.ManualSensor; +import fr.uca.iut.clfreville2.model.sensor.Sensor; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ReadOnlyListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.Iterator; +import java.util.function.IntSupplier; + +/** + * A registry class to hold thermal sensors instances. + */ +public class SensorRegistry implements Iterable { + + private final IntSupplier nextIdSupplier = new SequenceIntSupplier(); + private final ObservableList sensors = FXCollections.observableArrayList(); + private final ListProperty sensorsList = new SimpleListProperty<>(sensors); + + /** + * Create a new manual sensor and add it to the registry. + * + * @param name The name of the sensor. + * @return The sensor created. + */ + public ManualSensor createManual(String name) { + return register(new ManualSensor(nextIdSupplier.getAsInt(), name)); + } + + /** + * Add a new sensor to the registry. + * + * @param sensor The sensor to add. + * @return The sensor added. + * @param The type of sensor. + */ + protected T register(T sensor) { + sensorsList.add(sensor); + return sensor; + } + + /** + * Return a read-only list view of sensors. + * + * @return A list view of sensors. + */ + public ObservableList getSensors() { + return FXCollections.unmodifiableObservableList(sensors); + } + + /** + * Return the read-only list property of sensors. + * + * @return The list property of sensors. + */ + public ReadOnlyListProperty sensorsProperty() { + return sensorsList; + } + + @Override + public Iterator iterator() { + return getSensors().iterator(); + } +} diff --git a/src/fr/uca/iut/clfreville2/model/SequenceIntSupplier.java b/src/fr/uca/iut/clfreville2/model/SequenceIntSupplier.java new file mode 100644 index 0000000..03494bc --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/SequenceIntSupplier.java @@ -0,0 +1,18 @@ +package fr.uca.iut.clfreville2.model; + +import java.util.function.IntSupplier; + +/** + * A simple integer supplier that returns a sequence of integers. + * + * @implNote This class is not thread-safe. + */ +public class SequenceIntSupplier implements IntSupplier { + + private int current; + + @Override + public int getAsInt() { + return ++current; + } +} diff --git a/src/fr/uca/iut/clfreville2/model/binding/ToBooleanBinding.java b/src/fr/uca/iut/clfreville2/model/binding/ToBooleanBinding.java new file mode 100644 index 0000000..ba6ec04 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/binding/ToBooleanBinding.java @@ -0,0 +1,43 @@ +package fr.uca.iut.clfreville2.model.binding; + +import javafx.beans.Observable; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.value.ObservableObjectValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.concurrent.Callable; +import java.util.function.Predicate; + +/** + * Permet de se bind sur une valeur booléenne calculée sur un objet. + * + * @param Le type de l'objet observé. + * @implNote Basé sur {@link javafx.beans.binding.Bindings#createBooleanBinding(Callable, Observable...)} et {@link javafx.beans.binding.Bindings#isNull(ObservableObjectValue)}. + */ +public class ToBooleanBinding extends BooleanBinding { + + private final ObservableObjectValue target; + private final Predicate predicate; + + public ToBooleanBinding(ObservableObjectValue target, Predicate predicate) { + this.target = target; + this.predicate = predicate; + super.bind(target); + } + + @Override + public void dispose() { + super.unbind(target); + } + + @Override + protected boolean computeValue() { + return predicate.test(target.get()); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(target); + } +} diff --git a/src/fr/uca/iut/clfreville2/model/sensor/ManualSensor.java b/src/fr/uca/iut/clfreville2/model/sensor/ManualSensor.java new file mode 100644 index 0000000..84991bf --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/sensor/ManualSensor.java @@ -0,0 +1,20 @@ +package fr.uca.iut.clfreville2.model.sensor; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +/** + * A simple sensor implementation that can be used to manually set the temperature. + */ +public class ManualSensor extends Sensor { + + private final DoubleProperty temperature = new SimpleDoubleProperty(); + + public ManualSensor(int id, String name) { + super(id, name); + } + + public DoubleProperty temperatureProperty() { + return temperature; + } +} diff --git a/src/fr/uca/iut/clfreville2/model/sensor/Sensor.java b/src/fr/uca/iut/clfreville2/model/sensor/Sensor.java new file mode 100644 index 0000000..b8e0d43 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/sensor/Sensor.java @@ -0,0 +1,73 @@ +package fr.uca.iut.clfreville2.model.sensor; + +import fr.uca.iut.clfreville2.model.shared.TemperatureHolder; +import fr.uca.iut.clfreville2.model.shared.ObservableIdentifiable; +import javafx.beans.binding.DoubleBinding; +import javafx.beans.binding.DoubleExpression; +import javafx.beans.binding.StringExpression; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableDoubleValue; + +import static java.util.Objects.requireNonNull; + +/** + * A sensor base using properties. + */ +public abstract class Sensor implements ObservableIdentifiable, TemperatureHolder { + + private final IntegerProperty id = new SimpleIntegerProperty(); + private final StringProperty name = new SimpleStringProperty(); + private final StringExpression displayName = name.concat(" #").concat(id.asString()); + + public Sensor(int id, String name) { + this.id.set(id); + this.name.set(requireNonNull(name, "name")); + } + + /** + * Get the value binding of this sensor. + * + * @return The value binding. + */ + public abstract DoubleExpression temperatureProperty(); + + @Override + public double getTemperature() { + return temperatureProperty().get(); + } + + @Override + public int getId() { + return id.get(); + } + + @Override + public void setName(String name) { + this.name.set(requireNonNull(name, "name")); + } + + @Override + public String getName() { + return name.get(); + } + + @Override + public ReadOnlyIntegerProperty idProperty() { + return id; + } + + @Override + public StringProperty nameProperty() { + return name; + } + + @Override + public StringExpression displayNameExpression() { + return displayName; + } +} diff --git a/src/fr/uca/iut/clfreville2/model/sensor/package-info.java b/src/fr/uca/iut/clfreville2/model/sensor/package-info.java new file mode 100644 index 0000000..6261cfc --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/sensor/package-info.java @@ -0,0 +1,4 @@ +/** + * Represent different types of sensors. + */ +package fr.uca.iut.clfreville2.model.sensor; diff --git a/src/fr/uca/iut/clfreville2/model/shared/Identifiable.java b/src/fr/uca/iut/clfreville2/model/shared/Identifiable.java new file mode 100644 index 0000000..8d88990 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/shared/Identifiable.java @@ -0,0 +1,21 @@ +package fr.uca.iut.clfreville2.model.shared; + +/** + * An object that can be differentiated by an id. + */ +public interface Identifiable extends Nameable { + + /** + * Get the id of this object. + * + * @return The id. + */ + int getId(); + + /** + * Set the name of this object. + * + * @param name The name. + */ + void setName(String name); +} diff --git a/src/fr/uca/iut/clfreville2/model/shared/Nameable.java b/src/fr/uca/iut/clfreville2/model/shared/Nameable.java new file mode 100644 index 0000000..f329361 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/shared/Nameable.java @@ -0,0 +1,26 @@ +package fr.uca.iut.clfreville2.model.shared; + +/** + * Something that has a human-friendly name. + *

+ * This interface differs from {@link Object#toString()} in that it explicitly + * states that the name is not for debugging purposes. + */ +public interface Nameable { + + /** + * Get the name of this object. + * + * @return The name of this object. + */ + String getName(); + + /** + * Get the display name of this object. + * + * @return The display name. + */ + default String getDisplayName() { + return getName(); + } +} diff --git a/src/fr/uca/iut/clfreville2/model/shared/ObservableIdentifiable.java b/src/fr/uca/iut/clfreville2/model/shared/ObservableIdentifiable.java new file mode 100644 index 0000000..fa7d81f --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/shared/ObservableIdentifiable.java @@ -0,0 +1,35 @@ +package fr.uca.iut.clfreville2.model.shared; + +import javafx.beans.binding.StringExpression; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.StringProperty; + +/** + * An identifiable object that can be observed. + */ +public interface ObservableIdentifiable extends Identifiable { + + /** + * Get the id property of this object. + * + * @return The id property. + * @see #getId() + */ + ReadOnlyIntegerProperty idProperty(); + + /** + * Get the name property of this object. + * + * @return The name property. + * @see #getName() + */ + StringProperty nameProperty(); + + /** + * Get the display name of this object. + * + * @return The display name. + * @see #getDisplayName() + */ + StringExpression displayNameExpression(); +} diff --git a/src/fr/uca/iut/clfreville2/model/shared/TemperatureHolder.java b/src/fr/uca/iut/clfreville2/model/shared/TemperatureHolder.java new file mode 100644 index 0000000..d02c6d4 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/shared/TemperatureHolder.java @@ -0,0 +1,14 @@ +package fr.uca.iut.clfreville2.model.shared; + +/** + * An object that can have a numeric value. + */ +public interface TemperatureHolder { + + /** + * Get the temperature of this object. + * + * @return The temperature. + */ + double getTemperature(); +} diff --git a/src/fr/uca/iut/clfreville2/model/shared/package-info.java b/src/fr/uca/iut/clfreville2/model/shared/package-info.java new file mode 100644 index 0000000..1323f52 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/model/shared/package-info.java @@ -0,0 +1,4 @@ +/** + * Group commons interfaces and classes. + */ +package fr.uca.iut.clfreville2.model.shared; diff --git a/src/fr/uca/iut/clfreville2/persistence/SensorRegistryLoader.java b/src/fr/uca/iut/clfreville2/persistence/SensorRegistryLoader.java new file mode 100644 index 0000000..03f1c40 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/persistence/SensorRegistryLoader.java @@ -0,0 +1,14 @@ +package fr.uca.iut.clfreville2.persistence; + +import fr.uca.iut.clfreville2.model.SensorRegistry; + +@FunctionalInterface +public interface SensorRegistryLoader { + + /** + * Load the registry. + * + * @return The loaded registry. + */ + SensorRegistry load(); +} diff --git a/src/fr/uca/iut/clfreville2/persistence/SensorRegistrySaver.java b/src/fr/uca/iut/clfreville2/persistence/SensorRegistrySaver.java new file mode 100644 index 0000000..0be5566 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/persistence/SensorRegistrySaver.java @@ -0,0 +1,14 @@ +package fr.uca.iut.clfreville2.persistence; + +import fr.uca.iut.clfreville2.model.SensorRegistry; + +@FunctionalInterface +public interface SensorRegistrySaver { + + /** + * Save the registry. + * + * @param registry The registry to save. + */ + void save(SensorRegistry registry); +} diff --git a/src/fr/uca/iut/clfreville2/persistence/StubSensorRegistryLoader.java b/src/fr/uca/iut/clfreville2/persistence/StubSensorRegistryLoader.java new file mode 100644 index 0000000..f910142 --- /dev/null +++ b/src/fr/uca/iut/clfreville2/persistence/StubSensorRegistryLoader.java @@ -0,0 +1,14 @@ +package fr.uca.iut.clfreville2.persistence; + +import fr.uca.iut.clfreville2.model.SensorRegistry; + +public class StubSensorRegistryLoader implements SensorRegistryLoader { + @Override + public SensorRegistry load() { + SensorRegistry registry = new SensorRegistry(); + registry.createManual("Sensor 1"); + registry.createManual("Sensor 2"); + registry.createManual("Sensor 3"); + return registry; + } +}