Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transform an ObervableValue

Tags:

java

javafx

I need to transform an ObservableValue.

For instance: I have an ObervableStringValue and I want to display the length of the string in a JavaFX controll. When I do: value.get().length() i just get an int, but i need an ObservableValue.

So i quickly wrote a wrapper my own:

/**
 * Wraps an ObervableValue and offers a transformed value of it.
 * 
 * @param <F> From type.
 * @param <T> To type.
 */
public class TransformedValue<F, T> extends ObservableValueBase<T>
{
    private final ObservableValue<F> original;
    private final Function<F, T> function;

    /**
     * @param original ObservableValue to transform.
     * @param function Transform function.
     */
    public TransformedValue(ObservableValue<F> original, Function<F, T> function)
    {
        this.original = original;
        this.function = function;

        original.addListener((observable, oldValue, newValue) -> fireValueChangedEvent());
    }

    @Override
    public T getValue()
    {
        return function.apply(original.getValue());
    }
}

Usage:

new TransformedValue<>(someObservableStringValue, s -> s.length());

Here my questions:

  • Is my approach totaly stupid?
  • Is there a JavaFX way to do this?
  • Is there a third party library to do this?
  • Any suggestions to my code? (e.g. unregister listener)

Edit:

The example with String.length() was too simple, so here is the big story:

I have an ObservableList of sensors. Every sensor provides one ore more measurements, depending on type of sensor. The properties of a sensor are ObservabeValues. I display the sensors and their current measurements in a TreeTableView. Every sensor has its node and its measurements as subnodes. If will now focus on the timestamp column.

Initialisation of TreeTableColumns:

...
sensorTreeTimestamp.setCellValueFactory(cell -> cell.getValue().getValue().getTimestamp());
...

As there are totally different datatypes in the TreeTableView I have a own class SensorTreeValue to hold the data:

private static class SensorTreeValue
{
    ...
    private final ObservableValue<String> timestamp;
    ...

It has one constructor to represent a sensor and one for a measurement:

    private SensorTreeValue(Sensor sensor)
    {
        ...
        timestamp = new TransformedValue<>(sensor.getLastSeen(), (time) -> Utils.formatDateTime(time));
    }

    private SensorTreeValue(Sensor sensor, ValueType valueType)
    {
        ...
        timestamp = new TransformedValue<>(sensor.getLastMeasurement(), measure -> Utils.formatDateTime(measure.getTime()));
    }

I know there is a asString(format) function. But this is not enough because I still need to get the time out of the measurement and I didn't find a format string to transform a date to a locale formatted string.

I also could place the logic into the CellValueFactory but there I would have to do a type check if its a Sensor or a Measurement.

like image 924
Mr. Clear Avatar asked Jan 18 '26 22:01

Mr. Clear


2 Answers

Have a look at the EasyBind framework, which provides exactly this kind of functionality and whole lot more.

The example you suggest (creating an ObservableValue<Integer> representing the length of an ObservableValue<String>) is an example on the home page for the framework:

ObservableValue<String> text = ... ;
ObservableValue<Integer> textLength = EasyBind.map(text, String::length);

Use cases such as getting a "property of a property" are also shown on the project home page linked above.

like image 156
James_D Avatar answered Jan 20 '26 12:01

James_D


Here's a way of doing it, but your structure is too complicated. I don't know how you're adding items to your table. If it's a TreeTableView<SensorTreeItem> I think it's too complicated. I would only go the trouble of a custom item like that if there were multiple levels of nodes. Like for a file/directory view, it's necessary.

I would make every line in the table a measurement but have the sensor name in the measurement class. Then when adding to the table you make a simple node for the sensor name if it doesn't already exist and add it to that node.

import java.util.Date;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class FXTest extends Application {
    public static void main(String[] args) { launch(args); }

    @Override
    public void start(Stage stage) {
        Label lbl1 = new Label();
        Sensor sens1 = new Sensor();
        SensorTreeValue stv1 = new SensorTreeValue(sens1);
        lbl1.textProperty().bind(stv1.timeStamp.concat(" sens1"));

        Label lbl2 = new Label();
        Sensor sens2 = new Sensor();
        SensorTreeValue stv2 = new SensorTreeValue(sens2, 0);
        lbl2.textProperty().bind(stv2.timeStamp.concat(" sens2"));

        Scene scene = new Scene(new VBox(5, lbl1, lbl2));
        stage.setScene(scene);
        stage.show();

        Timeline timer = new Timeline(new KeyFrame(
            javafx.util.Duration.millis(1000), ae -> {
                sens1.lastSeen.set(System.currentTimeMillis());
                sens2.lastMeasurement.get().time.set(System.currentTimeMillis());
            }));
        timer.setCycleCount(Animation.INDEFINITE);
        timer.play();
    }

    private static class SensorTreeValue {
        private final SimpleStringProperty timeStamp = new SimpleStringProperty();

        private SensorTreeValue(Sensor sensor) {
            //you can bind or set a property, not an ObsValue<T>
            timeStamp.bind(Bindings.createStringBinding(() -> {
                return new Date(sensor.lastSeen.get()).toString();
            },sensor.lastSeen));
        }

        private SensorTreeValue(Sensor sensor, int valueType) {
            timeStamp.bind(Bindings.createStringBinding(() -> {
                return new Date(sensor.lastMeasurement.get().time.get()).toString();
            },sensor.lastMeasurement.get().time));
        }

    }

    private static class Sensor{
        private SimpleLongProperty lastSeen = new SimpleLongProperty();
        private SimpleObjectProperty<Measure> lastMeasurement = new SimpleObjectProperty<>(new Measure());
    }

    private static class Measure{
        private SimpleLongProperty time = new SimpleLongProperty();
        private SimpleLongProperty value = new SimpleLongProperty();
    }
}
like image 36
brian Avatar answered Jan 20 '26 10:01

brian