Initial commit

This commit is contained in:
2022-12-15 10:26:04 +01:00
commit cf47f08d5e
26 changed files with 681 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -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

13
Ebullition.iml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="library" name="OpenJFX" level="application" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Button?>
<SplitPane xmlns:fx="http://javafx.com/fxml" prefHeight="400.0" prefWidth="600.0"
fx:controller="fr.uca.iut.clfreville2.gui.MainWindows">
<ListView fx:id="sensorsList" />
<VBox alignment="TOP_CENTER">
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
</padding>
<Text fx:id="sensorId" text="Details" />
<TextField fx:id="sensorName" />
<BorderPane>
<left>
<Button fx:id="changeBtn" onAction="#onChangeClick" visible="false">Change</Button>
</left>
<right>
<Button fx:id="visualizeBtn" onAction="#onVisualizeClick">Visualize</Button>
</right>
</BorderPane>
</VBox>
</SplitPane>

View File

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

View File

@@ -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<Sensor> 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;
}
}

View File

@@ -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<Pane> 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<Pane> 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);
});
}
}

View File

@@ -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<Sensor, Image> {
}

View File

@@ -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;
}
}

View File

@@ -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<T extends ObservableIdentifiable> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
textProperty().unbind();
} else {
textProperty().bind(item.displayNameExpression());
}
}
}

View File

@@ -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<Sensor> {
private final IntSupplier nextIdSupplier = new SequenceIntSupplier();
private final ObservableList<Sensor> sensors = FXCollections.observableArrayList();
private final ListProperty<Sensor> 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 <T> The type of sensor.
*/
protected <T extends Sensor> 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<Sensor> getSensors() {
return FXCollections.unmodifiableObservableList(sensors);
}
/**
* Return the read-only list property of sensors.
*
* @return The list property of sensors.
*/
public ReadOnlyListProperty<Sensor> sensorsProperty() {
return sensorsList;
}
@Override
public Iterator<Sensor> iterator() {
return getSensors().iterator();
}
}

View File

@@ -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;
}
}

View File

@@ -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 <T> 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<T> extends BooleanBinding {
private final ObservableObjectValue<T> target;
private final Predicate<T> predicate;
public ToBooleanBinding(ObservableObjectValue<T> target, Predicate<T> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Represent different types of sensors.
*/
package fr.uca.iut.clfreville2.model.sensor;

View File

@@ -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);
}

View File

@@ -0,0 +1,26 @@
package fr.uca.iut.clfreville2.model.shared;
/**
* Something that has a human-friendly name.
* <p>
* 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();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* Group commons interfaces and classes.
*/
package fr.uca.iut.clfreville2.model.shared;

View File

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

View File

@@ -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);
}

View File

@@ -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;
}
}