From 35093e4003119e0acb7bb55a8cfd23d9f2d4ac74 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 28 Nov 2025 18:42:00 +0100 Subject: [PATCH 01/23] Setup base APIs to show dialog comparing stored array and live array --- .../ui/snapshot/compare/ColumnEntry.java | 23 ++++ .../ui/snapshot/compare/ComparisonData.java | 41 ++++++ .../ui/snapshot/compare/ComparisonDialog.java | 34 +++++ .../TableComparisonViewController.java | 117 ++++++++++++++++++ .../snapshot/compare/TableComparisonView.fxml | 28 +++++ .../compare/ComparisonDialogDemo.java | 33 +++++ 6 files changed, 276 insertions(+) create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java create mode 100644 app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java create mode 100644 app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml create mode 100644 app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java new file mode 100644 index 0000000000..0add53ebbf --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.snapshot.compare; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +public class ColumnEntry { + + private final ObjectProperty snapshotVal = new SimpleObjectProperty<>(this, "snapshotValue", null); + private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); + private final ObjectProperty liveVal = new SimpleObjectProperty<>(this, "liveValue", null); + + public ColumnEntry(T snapshotVal){ + this.snapshotVal.set(snapshotVal); + } + + public ObjectProperty getSnapshotValue(){ + return snapshotVal; + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java new file mode 100644 index 0000000000..836c0416f6 --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.snapshot.compare; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import org.epics.vtype.VBooleanArray; +import org.epics.vtype.VByteArray; +import org.epics.vtype.VDoubleArray; +import org.epics.vtype.VFloatArray; +import org.epics.vtype.VIntArray; +import org.epics.vtype.VLongArray; +import org.epics.vtype.VNumberArray; +import org.epics.vtype.VShortArray; +import org.epics.vtype.VTable; +import org.epics.vtype.VType; + +import java.util.ArrayList; +import java.util.List; + +public class ComparisonData { + + private final IntegerProperty index = new SimpleIntegerProperty(this, "index"); + private List columnEntries; + + public ComparisonData(int index, List columnEntries){ + this.index.set(index); + this.columnEntries = columnEntries; + } + + @SuppressWarnings("unused") + public IntegerProperty indexProperty() { + return index; + } + + public List getColumnEntries(){ + return columnEntries; + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java new file mode 100644 index 0000000000..d10b3b748c --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.snapshot.compare; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.Dialog; +import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.Messages; +import org.phoebus.framework.nls.NLS; + +import java.io.IOException; +import java.util.ResourceBundle; + +public class ComparisonDialog extends Dialog { + + public ComparisonDialog(VType data, String pvName){ + + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); + FXMLLoader loader = new FXMLLoader(); + loader.setResources(resourceBundle); + loader.setLocation(this.getClass().getResource("TableComparisonView.fxml")); + try { + Node node = loader.load(); + TableComparisonViewController controller = loader.getController(); + controller.loadDataAndConnect(data, pvName); + getDialogPane().setContent(node); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java new file mode 100644 index 0000000000..aaa0c381ac --- /dev/null +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.snapshot.compare; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import org.epics.util.array.IteratorNumber; +import org.epics.util.array.ListBoolean; +import org.epics.vtype.VBooleanArray; +import org.epics.vtype.VByteArray; +import org.epics.vtype.VDoubleArray; +import org.epics.vtype.VFloatArray; +import org.epics.vtype.VIntArray; +import org.epics.vtype.VLongArray; +import org.epics.vtype.VNumberArray; +import org.epics.vtype.VShortArray; +import org.epics.vtype.VType; + +import java.util.ArrayList; +import java.util.List; + +public class TableComparisonViewController { + + @SuppressWarnings("unused") + @FXML + private TableView comparisonTable; + + @SuppressWarnings("unused") + @FXML + private TableColumn valueColumn; + + @FXML + public void initialize(){ + comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); + + valueColumn.setCellValueFactory(cell -> + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getSnapshotValue()); + } + + public void loadDataAndConnect(VType data, String pvName){ + + if(data instanceof VNumberArray){ + IteratorNumber iteratorNumber = ((VNumberArray)data).getData().iterator(); + List columnEntries = new ArrayList<>(); + int index = 0; + if(data instanceof VDoubleArray){ + while(iteratorNumber.hasNext()){ + double value = iteratorNumber.nextDouble(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VFloatArray){ + while(iteratorNumber.hasNext()){ + float value = iteratorNumber.nextFloat(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VIntArray){ + while(iteratorNumber.hasNext()){ + int value = iteratorNumber.nextInt(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VLongArray){ + while(iteratorNumber.hasNext()){ + long value = iteratorNumber.nextLong(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VShortArray){ + while(iteratorNumber.hasNext()){ + short value = iteratorNumber.nextShort(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VByteArray){ + while(iteratorNumber.hasNext()){ + byte value = iteratorNumber.nextByte(); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + index++; + } + } + else if(data instanceof VBooleanArray){ + ListBoolean listBoolean = ((VBooleanArray)data).getData(); + for(int i = 0; i < listBoolean.size(); i++){ + boolean value = listBoolean.getBoolean(i); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(index, columnEntries, columnEntry); + } + } + } + + } + + private void addRow(int index, List columnEntries, ColumnEntry columnEntry){ + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } + + +} diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml new file mode 100644 index 0000000000..6469919b52 --- /dev/null +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -0,0 +1,28 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java new file mode 100644 index 0000000000..3e5dc7f5ba --- /dev/null +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.ui.snapshot.compare; + +import javafx.stage.Stage; +import org.epics.util.array.ArrayDouble; +import org.epics.util.array.ListDouble; +import org.epics.vtype.Alarm; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VDoubleArray; +import org.phoebus.ui.javafx.ApplicationWrapper; + +public class ComparisonDialogDemo extends ApplicationWrapper { + + public static void main(String[] args){ + launch(ComparisonDialogDemo.class, args); + } + + @Override + public void start(Stage primaryStage) throws Exception { + VDoubleArray vDoubleArray = + VDoubleArray.of(ArrayDouble.of(1.0, 2.0, 3.0, 4.0, 5.0), + Alarm.none(), + Time.now(), + Display.none()); + ComparisonDialog comparisonDialog = + new ComparisonDialog(vDoubleArray, ""); + comparisonDialog.show(); + } +} From 2f5314b4163e12ae162cc98877030c1b4dbd7f1c Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 1 Dec 2025 13:25:19 +0100 Subject: [PATCH 02/23] Add pv name to dialog layout --- .../ui/snapshot/compare/ComparisonDialog.java | 3 +++ .../TableComparisonViewController.java | 6 +++++ .../snapshot/compare/TableComparisonView.fxml | 22 ++++++++++++------- .../compare/ComparisonDialogDemo.java | 4 ++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java index d10b3b748c..53702b4385 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -6,6 +6,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Node; +import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; import org.epics.vtype.VType; import org.phoebus.applications.saveandrestore.Messages; @@ -18,6 +19,8 @@ public class ComparisonDialog extends Dialog { public ComparisonDialog(VType data, String pvName){ + getDialogPane().getButtonTypes().addAll(ButtonType.OK); + ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader loader = new FXMLLoader(); loader.setResources(resourceBundle); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index aaa0c381ac..4d937f5f70 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -6,6 +6,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import org.epics.util.array.IteratorNumber; @@ -33,6 +34,9 @@ public class TableComparisonViewController { @FXML private TableColumn valueColumn; + @FXML + private Label pvName; + @FXML public void initialize(){ comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); @@ -43,6 +47,8 @@ public void initialize(){ public void loadDataAndConnect(VType data, String pvName){ + this.pvName.textProperty().set(pvName); + if(data instanceof VNumberArray){ IteratorNumber iteratorNumber = ((VNumberArray)data).getData().iterator(); List columnEntries = new ArrayList<>(); diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index 6469919b52..f50e316e2d 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -1,28 +1,34 @@ + + - - +
- + - - - + + +
+ + +
diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 3e5dc7f5ba..82e88c11ea 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -22,12 +22,12 @@ public static void main(String[] args){ @Override public void start(Stage primaryStage) throws Exception { VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(1.0, 2.0, 3.0, 4.0, 5.0), + VDoubleArray.of(ArrayDouble.of(5.0, 6.0, 7.0, 8.0, 9.0), Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, ""); + new ComparisonDialog(vDoubleArray, "MUSIGNY"); comparisonDialog.show(); } } From ab385da8302931c061bc6364ea48e02df7cd5802 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 1 Dec 2025 15:11:33 +0100 Subject: [PATCH 03/23] Update comparison table when PV is connected --- .../ui/snapshot/compare/ColumnEntry.java | 12 +- .../TableComparisonViewController.java | 129 ++++++++++++++---- .../snapshot/compare/TableComparisonView.fxml | 2 +- .../compare/ComparisonDialogDemo.java | 7 +- 4 files changed, 116 insertions(+), 34 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 0add53ebbf..098a169aa6 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -6,12 +6,14 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VDisconnectedData; public class ColumnEntry { private final ObjectProperty snapshotVal = new SimpleObjectProperty<>(this, "snapshotValue", null); private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); - private final ObjectProperty liveVal = new SimpleObjectProperty<>(this, "liveValue", null); + private final ObjectProperty liveVal = new SimpleObjectProperty<>(this, "liveValue", (T) VDisconnectedData.INSTANCE); public ColumnEntry(T snapshotVal){ this.snapshotVal.set(snapshotVal); @@ -20,4 +22,12 @@ public ColumnEntry(T snapshotVal){ public ObjectProperty getSnapshotValue(){ return snapshotVal; } + + public void setLiveVal(T value){ + liveVal.set(value); + } + + public ObjectProperty getLiveValue(){ + return liveVal; + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 4d937f5f70..c9be0cf2d3 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -4,7 +4,9 @@ package org.phoebus.applications.saveandrestore.ui.snapshot.compare; -import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; @@ -14,15 +16,23 @@ import org.epics.vtype.VBooleanArray; import org.epics.vtype.VByteArray; import org.epics.vtype.VDoubleArray; +import org.epics.vtype.VEnumArray; import org.epics.vtype.VFloatArray; import org.epics.vtype.VIntArray; import org.epics.vtype.VLongArray; import org.epics.vtype.VNumberArray; import org.epics.vtype.VShortArray; +import org.epics.vtype.VStringArray; import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; public class TableComparisonViewController { @@ -34,90 +44,149 @@ public class TableComparisonViewController { @FXML private TableColumn valueColumn; + @SuppressWarnings("unused") + @FXML + private TableColumn liveValueColumn; + + @SuppressWarnings("unused") @FXML private Label pvName; + private final StringProperty pvNameProperty = new SimpleStringProperty(); + + /** + * The time between updates of dynamic data in the table, in ms. + */ + private static final long TABLE_UPDATE_INTERVAL = 500; + @FXML - public void initialize(){ + public void initialize() { comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); - + pvName.textProperty().bind(pvNameProperty); valueColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getSnapshotValue()); + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getSnapshotValue()); + liveValueColumn.setCellValueFactory(cell -> + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getLiveValue()); } - public void loadDataAndConnect(VType data, String pvName){ + public void loadDataAndConnect(VType data, String pvName) { - this.pvName.textProperty().set(pvName); + pvNameProperty.set(pvName); - if(data instanceof VNumberArray){ - IteratorNumber iteratorNumber = ((VNumberArray)data).getData().iterator(); + if (data instanceof VNumberArray) { + IteratorNumber iteratorNumber = ((VNumberArray) data).getData().iterator(); List columnEntries = new ArrayList<>(); int index = 0; - if(data instanceof VDoubleArray){ - while(iteratorNumber.hasNext()){ + if (data instanceof VDoubleArray) { + while (iteratorNumber.hasNext()) { double value = iteratorNumber.nextDouble(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VFloatArray){ - while(iteratorNumber.hasNext()){ + } else if (data instanceof VFloatArray) { + while (iteratorNumber.hasNext()) { float value = iteratorNumber.nextFloat(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VIntArray){ - while(iteratorNumber.hasNext()){ + } else if (data instanceof VIntArray) { + while (iteratorNumber.hasNext()) { int value = iteratorNumber.nextInt(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VLongArray){ - while(iteratorNumber.hasNext()){ + } else if (data instanceof VLongArray) { + while (iteratorNumber.hasNext()) { long value = iteratorNumber.nextLong(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VShortArray){ - while(iteratorNumber.hasNext()){ + } else if (data instanceof VShortArray) { + while (iteratorNumber.hasNext()) { short value = iteratorNumber.nextShort(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VByteArray){ - while(iteratorNumber.hasNext()){ + } else if (data instanceof VByteArray) { + while (iteratorNumber.hasNext()) { byte value = iteratorNumber.nextByte(); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); index++; } - } - else if(data instanceof VBooleanArray){ - ListBoolean listBoolean = ((VBooleanArray)data).getData(); - for(int i = 0; i < listBoolean.size(); i++){ + } else if (data instanceof VBooleanArray) { + ListBoolean listBoolean = ((VBooleanArray) data).getData(); + for (int i = 0; i < listBoolean.size(); i++) { boolean value = listBoolean.getBoolean(i); ColumnEntry columnEntry = new ColumnEntry<>(value); addRow(index, columnEntries, columnEntry); } + } else if (data instanceof VEnumArray) { + List enumValues = ((VEnumArray) data).getData(); + for (int i = 0; i < enumValues.size(); i++) { + ColumnEntry columnEntry = new ColumnEntry<>(enumValues.get(i)); + addRow(index, columnEntries, columnEntry); + } } } + connect(); + } - private void addRow(int index, List columnEntries, ColumnEntry columnEntry){ + private void addRow(int index, List columnEntries, ColumnEntry columnEntry) { columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); } + public void connect() { + try { + PV pv = PVPool.getPV(pvNameProperty.get()); + pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) + .subscribe(value -> updateTable(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); + } catch (Exception e) { + Logger.getLogger(TableComparisonViewController.class.getName()).log(Level.INFO, "Error connecting to PV", e); + } + } + private void updateTable(VType liveData) { + if (liveData.equals(VDisconnectedData.INSTANCE)) { + comparisonTable.getItems().forEach(i -> i.getColumnEntries().get(0).setLiveVal(VDisconnectedData.INSTANCE)); + } else { + comparisonTable.getItems().forEach(i -> { + int index = i.indexProperty().get(); + if (liveData instanceof VDoubleArray) { + VDoubleArray array = (VDoubleArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getDouble(index)); + } else if (liveData instanceof VIntArray) { + VIntArray array = (VIntArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getInt(index)); + } else if (liveData instanceof VLongArray) { + VLongArray array = (VLongArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getLong(index)); + } else if (liveData instanceof VFloatArray) { + VFloatArray array = (VFloatArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getFloat(index)); + } else if (liveData instanceof VShortArray) { + VShortArray array = (VShortArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getShort(index)); + } else if (liveData instanceof VBooleanArray) { + VBooleanArray array = (VBooleanArray)liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().getBoolean(index)); + } else if (liveData instanceof VEnumArray) { + VEnumArray array = (VEnumArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); + } else if (liveData instanceof VStringArray) { + VStringArray array = (VStringArray) liveData; + i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); + } + }); + } + } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index f50e316e2d..687dc0fb49 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -18,7 +18,7 @@ - + diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 82e88c11ea..1c6d17a398 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -11,6 +11,8 @@ import org.epics.vtype.Display; import org.epics.vtype.Time; import org.epics.vtype.VDoubleArray; +import org.phoebus.pv.PVFactory; +import org.phoebus.pv.PVPool; import org.phoebus.ui.javafx.ApplicationWrapper; public class ComparisonDialogDemo extends ApplicationWrapper { @@ -21,13 +23,14 @@ public static void main(String[] args){ @Override public void start(Stage primaryStage) throws Exception { + VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(5.0, 6.0, 7.0, 8.0, 9.0), + VDoubleArray.of(ArrayDouble.of(1.0, 6.0, 7.0, 8.0, 9.0), Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "MUSIGNY"); + new ComparisonDialog(vDoubleArray, "loc://x(1,2,3,4,15)"); comparisonDialog.show(); } } From c9bacba6250cf2aee0b59dbf61b29fc769c75fc1 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 2 Dec 2025 15:13:07 +0100 Subject: [PATCH 04/23] Render delta value in comparison dialog --- .../applications/saveandrestore/Messages.java | 2 + .../ui/snapshot/compare/ColumnEntry.java | 39 +++++++++- .../TableComparisonViewController.java | 75 ++++++++++++------- .../saveandrestore/messages.properties | 2 + .../snapshot/compare/TableComparisonView.fxml | 6 +- .../compare/ComparisonDialogDemo.java | 10 ++- .../saveandrestore/util/Utilities.java | 4 +- 7 files changed, 102 insertions(+), 36 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index db0047186a..45ce27edbc 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -107,6 +107,7 @@ public class Messages { public static String importSnapshotLabel; public static String includeThisPV; public static String inverseSelection; + public static String live; public static String liveReadbackVsSetpoint; public static String liveSetpoint; public static String login; @@ -159,6 +160,7 @@ public class Messages { public static String snapshotFromPvs; public static String status; public static String storedReadbackValue; + public static String stored; public static String storedValues; public static String tableColumnDeltaValue; public static String tagAddFailed; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 098a169aa6..1b0ddcdddd 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -8,11 +8,12 @@ import javafx.beans.property.SimpleObjectProperty; import org.epics.vtype.VType; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.saveandrestore.util.Utilities; public class ColumnEntry { private final ObjectProperty snapshotVal = new SimpleObjectProperty<>(this, "snapshotValue", null); - private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); + private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); private final ObjectProperty liveVal = new SimpleObjectProperty<>(this, "liveValue", (T) VDisconnectedData.INSTANCE); public ColumnEntry(T snapshotVal){ @@ -25,9 +26,45 @@ public ObjectProperty getSnapshotValue(){ public void setLiveVal(T value){ liveVal.set(value); + if(value instanceof String){ + String stringValue = (String)value; + delta.set(new ColumnDelta(stringValue, stringValue.equals(snapshotVal.get()))); + } + else{ + Double valueNumber = ((Number) value).doubleValue(); + Double diff = ((Number) snapshotVal.get()).doubleValue() - valueNumber; + String diffString = Double.toString(diff); + if(diff > 0){ + diffString = "+" + diff; + } + delta.set(new ColumnDelta(diffString, diff != 0)); + } } public ObjectProperty getLiveValue(){ return liveVal; } + + public ObjectProperty getDelta(){ + return delta; + } + + public static class ColumnDelta{ + private String deltaString; + private boolean equal; + + public ColumnDelta(String deltaString, boolean equal){ + this.deltaString = deltaString; + this.equal = equal; + } + + public boolean isEqual() { + return equal; + } + + @Override + public String toString(){ + return deltaString; + } + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index c9be0cf2d3..e23d250bd7 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -27,6 +27,7 @@ import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; +import org.phoebus.saveandrestore.util.Utilities; import java.util.ArrayList; import java.util.List; @@ -42,12 +43,17 @@ public class TableComparisonViewController { @SuppressWarnings("unused") @FXML - private TableColumn valueColumn; + private TableColumn storedValueColumn; @SuppressWarnings("unused") @FXML private TableColumn liveValueColumn; + + @SuppressWarnings("unused") + @FXML + private TableColumn deltaColumn; + @SuppressWarnings("unused") @FXML private Label pvName; @@ -63,20 +69,21 @@ public class TableComparisonViewController { public void initialize() { comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); pvName.textProperty().bind(pvNameProperty); - valueColumn.setCellValueFactory(cell -> + storedValueColumn.setCellValueFactory(cell -> cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getSnapshotValue()); liveValueColumn.setCellValueFactory(cell -> cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getLiveValue()); + deltaColumn.setCellValueFactory(cell -> + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getDelta()); } public void loadDataAndConnect(VType data, String pvName) { pvNameProperty.set(pvName); - + List columnEntries = new ArrayList<>(); if (data instanceof VNumberArray) { - IteratorNumber iteratorNumber = ((VNumberArray) data).getData().iterator(); - List columnEntries = new ArrayList<>(); int index = 0; + IteratorNumber iteratorNumber = ((VNumberArray) data).getData().iterator(); if (data instanceof VDoubleArray) { while (iteratorNumber.hasNext()) { double value = iteratorNumber.nextDouble(); @@ -119,19 +126,20 @@ public void loadDataAndConnect(VType data, String pvName) { addRow(index, columnEntries, columnEntry); index++; } - } else if (data instanceof VBooleanArray) { - ListBoolean listBoolean = ((VBooleanArray) data).getData(); - for (int i = 0; i < listBoolean.size(); i++) { - boolean value = listBoolean.getBoolean(i); - ColumnEntry columnEntry = new ColumnEntry<>(value); - addRow(index, columnEntries, columnEntry); - } - } else if (data instanceof VEnumArray) { - List enumValues = ((VEnumArray) data).getData(); - for (int i = 0; i < enumValues.size(); i++) { - ColumnEntry columnEntry = new ColumnEntry<>(enumValues.get(i)); - addRow(index, columnEntries, columnEntry); - } + } + } + else if (data instanceof VBooleanArray) { + ListBoolean listBoolean = ((VBooleanArray) data).getData(); + for (int i = 0; i < listBoolean.size(); i++) { + boolean value = listBoolean.getBoolean(i); + ColumnEntry columnEntry = new ColumnEntry<>(value); + addRow(i, columnEntries, columnEntry); + } + } else if (data instanceof VEnumArray) { + List enumValues = ((VEnumArray) data).getData(); + for (int i = 0; i < enumValues.size(); i++) { + ColumnEntry columnEntry = new ColumnEntry<>(enumValues.get(i)); + addRow(i, columnEntries, columnEntry); } } @@ -161,31 +169,46 @@ private void updateTable(VType liveData) { } else { comparisonTable.getItems().forEach(i -> { int index = i.indexProperty().get(); + ColumnEntry columnEntry = i.getColumnEntries().get(index); if (liveData instanceof VDoubleArray) { VDoubleArray array = (VDoubleArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getDouble(index)); - } else if (liveData instanceof VIntArray) { + double value = array.getData().getDouble(index); + columnEntry.setLiveVal(value); + double absoluteDelta = (Double)columnEntry.getSnapshotValue().get() - value; + String deltaString = (absoluteDelta > 0 ? "+" : "-") + absoluteDelta; + //columnEntry.setDelta(new Utilities.VTypeComparison(deltaString, absoluteDelta > 0 ? 1 : -1, false, absoluteDelta)); + } /*else if (liveData instanceof VIntArray) { VIntArray array = (VIntArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getInt(index)); + int value = array.getData().getInt(index); + columnEntry.setLiveVal(value); + columnEntry.setDelta(((Integer)columnEntry.getSnapshotValue().get()) - value); } else if (liveData instanceof VLongArray) { VLongArray array = (VLongArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getLong(index)); + long value = array.getData().getLong(index); + columnEntry.setLiveVal(value); + columnEntry.setDelta(((Long)columnEntry.getSnapshotValue().get()) - value); } else if (liveData instanceof VFloatArray) { VFloatArray array = (VFloatArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getFloat(index)); + float value = array.getData().getFloat(index); + columnEntry.setLiveVal(value); + columnEntry.setDelta(((Float)columnEntry.getSnapshotValue().get()) - value); } else if (liveData instanceof VShortArray) { VShortArray array = (VShortArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getShort(index)); + short value = array.getData().getShort(index); + columnEntry.setLiveVal(value); + columnEntry.setDelta(((Short)columnEntry.getSnapshotValue().get()) - value); } else if (liveData instanceof VBooleanArray) { VBooleanArray array = (VBooleanArray)liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().getBoolean(index)); + boolean value = array.getData().getBoolean(index); + columnEntry.setLiveVal(value); + //columnEntry.setDelta(((Boolean)columnEntry.getSnapshotValue().get()) - value); } else if (liveData instanceof VEnumArray) { VEnumArray array = (VEnumArray) liveData; i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); } else if (liveData instanceof VStringArray) { VStringArray array = (VStringArray) liveData; i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); - } + }*/ }); } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 7371d54ca7..c97215921b 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -124,6 +124,7 @@ jmasarServiceUnavailable=Save-and-restore service unavailable! labelMultiplier=Restore with scale labelThreshold=\u0394 Threshold (%) lastModifiedDate=Last Modified +live=Live liveReadbackVsSetpoint=Live Readback\n(? Live Setpoint) liveSetpoint=Live Setpoint logAction=Create Log Entry @@ -216,6 +217,7 @@ snapshotLocation=Location snapshotName=Name snapshotNameFieldHint=Enter a name (case-sensitive) status=Status +stored=Stored storedReadbackValue=Stored Readback Value storedValues=Stored Setpoints tableColumnAlarmSeverity=Alarm Severity diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index 687dc0fb49..8f6efe2cc6 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -16,9 +16,9 @@ - - - + + + diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 1c6d17a398..33fc4730a6 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -5,12 +5,15 @@ package org.phoebus.applications.saveandrestore.ui.snapshot.compare; import javafx.stage.Stage; +import org.epics.util.array.ArrayBoolean; import org.epics.util.array.ArrayDouble; import org.epics.util.array.ListDouble; import org.epics.vtype.Alarm; import org.epics.vtype.Display; import org.epics.vtype.Time; +import org.epics.vtype.VBooleanArray; import org.epics.vtype.VDoubleArray; +import org.epics.vtype.VIntArray; import org.phoebus.pv.PVFactory; import org.phoebus.pv.PVPool; import org.phoebus.ui.javafx.ApplicationWrapper; @@ -25,12 +28,11 @@ public static void main(String[] args){ public void start(Stage primaryStage) throws Exception { VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(1.0, 6.0, 7.0, 8.0, 9.0), + VDoubleArray.of(ArrayDouble.of(1, 2,3, 4, 5), Alarm.none(), - Time.now(), - Display.none()); + Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(1,2,3,4,15)"); + new ComparisonDialog(vDoubleArray, "loc://x(1, 8, 7, 0, -1)"); comparisonDialog.show(); } } diff --git a/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/Utilities.java b/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/Utilities.java index 9d6283cc69..8329b7c09a 100644 --- a/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/Utilities.java +++ b/app/save-and-restore/util/src/main/java/org/phoebus/saveandrestore/util/Utilities.java @@ -97,13 +97,13 @@ public static class VTypeComparison { private final boolean withinThreshold; private double absoluteDelta = 0.0; - VTypeComparison(String string, int equal, boolean withinThreshold) { + public VTypeComparison(String string, int equal, boolean withinThreshold) { this.string = string; this.valuesEqual = equal; this.withinThreshold = withinThreshold; } - VTypeComparison(String string, int equal, boolean withinThreshold, double absoluteDelta) { + public VTypeComparison(String string, int equal, boolean withinThreshold, double absoluteDelta) { this.string = string; this.valuesEqual = equal; this.withinThreshold = withinThreshold; From 4838f2b004f3b444ac732811df321e453f7090b0 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 2 Dec 2025 15:25:10 +0100 Subject: [PATCH 05/23] Add styling of diff column --- .../ui/snapshot/compare/ColumnEntry.java | 2 +- .../ui/snapshot/compare/ComparisonDialog.java | 1 + .../compare/TableComparisonViewController.java | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 1b0ddcdddd..779c58823e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -37,7 +37,7 @@ public void setLiveVal(T value){ if(diff > 0){ diffString = "+" + diff; } - delta.set(new ColumnDelta(diffString, diff != 0)); + delta.set(new ColumnDelta(diffString, diff == 0)); } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java index 53702b4385..c76336faad 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -20,6 +20,7 @@ public class ComparisonDialog extends Dialog { public ComparisonDialog(VType data, String pvName){ getDialogPane().getButtonTypes().addAll(ButtonType.OK); + setResizable(true); ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader loader = new FXMLLoader(); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index e23d250bd7..1f3c951065 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -9,6 +9,7 @@ import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import org.epics.util.array.IteratorNumber; @@ -24,6 +25,7 @@ import org.epics.vtype.VShortArray; import org.epics.vtype.VStringArray; import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.ui.snapshot.TableCellColors; import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; @@ -75,6 +77,22 @@ public void initialize() { cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getLiveValue()); deltaColumn.setCellValueFactory(cell -> cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getDelta()); + + deltaColumn.setCellFactory(e -> new TableCell<>(){ + @Override + public void updateItem(ColumnEntry.ColumnDelta item, boolean empty) { + if(item != null && !empty){ + if(!item.isEqual()){ + setStyle(TableCellColors.ALARM_MAJOR_STYLE); + } + else{ + setStyle(TableCellColors.REGULAR_CELL_STYLE); + } + setText(item.toString()); + } + + } + }); } public void loadDataAndConnect(VType data, String pvName) { From 78385ca29e03f842dacecf9fd71b5a801aea686f Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 2 Dec 2025 15:29:10 +0100 Subject: [PATCH 06/23] Adding comparison dialog title --- .../java/org/phoebus/applications/saveandrestore/Messages.java | 1 + .../saveandrestore/ui/snapshot/compare/ComparisonDialog.java | 3 ++- .../phoebus/applications/saveandrestore/messages.properties | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index 45ce27edbc..24c11f962e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -39,6 +39,7 @@ public class Messages { public static String closeConfigurationTabPrompt; public static String closeSnapshotTabPrompt; public static String compositeSnapshotConsistencyCheckFailed; + public static String comparisonDialogTitle; public static String contextMenuAddTag; @Deprecated public static String contextMenuAddTagWithComment; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java index c76336faad..3af5d045bc 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -19,8 +19,9 @@ public class ComparisonDialog extends Dialog { public ComparisonDialog(VType data, String pvName){ - getDialogPane().getButtonTypes().addAll(ButtonType.OK); + getDialogPane().getButtonTypes().addAll(ButtonType.CLOSE); setResizable(true); + setTitle(Messages.comparisonDialogTitle); ResourceBundle resourceBundle = NLS.getMessages(Messages.class); FXMLLoader loader = new FXMLLoader(); diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index c97215921b..1cb6023b65 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -29,6 +29,7 @@ copy=Copy createdDate=Created createLogEntry=Create Log Entry createLogEntryToolTip=Create a log entry when snapshot has been saved or restored +comparisonDialogTitle=Comparing array/table data compositeSnapshotName=Composite Snapshot Name configurationLocation=Location configurationName=Configuration Name From 85b257dbf0284ef17fc5642cf8931ee4657f367e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 2 Dec 2025 16:37:31 +0100 Subject: [PATCH 07/23] Additional string resources --- .../java/org/phoebus/applications/saveandrestore/Messages.java | 1 + .../org/phoebus/applications/saveandrestore/messages.properties | 1 + 2 files changed, 2 insertions(+) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index 24c11f962e..8dd7ac7039 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -39,6 +39,7 @@ public class Messages { public static String closeConfigurationTabPrompt; public static String closeSnapshotTabPrompt; public static String compositeSnapshotConsistencyCheckFailed; + public static String comparisonDialogLunchError; public static String comparisonDialogTitle; public static String contextMenuAddTag; @Deprecated diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 1cb6023b65..6d19e32639 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -29,6 +29,7 @@ copy=Copy createdDate=Created createLogEntry=Create Log Entry createLogEntryToolTip=Create a log entry when snapshot has been saved or restored +comparisonDialogLunchError=Failed to create comparison dialog comparisonDialogTitle=Comparing array/table data compositeSnapshotName=Composite Snapshot Name configurationLocation=Location From e3a5f0954cf3742ae0a9c9ca56148563f8a108d9 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 3 Dec 2025 11:09:53 +0100 Subject: [PATCH 08/23] More styling and fixed sorting on delta column --- .../ui/snapshot/compare/ColumnEntry.java | 55 +++++++++++-------- .../TableComparisonViewController.java | 50 +++++++---------- .../snapshot/compare/TableComparisonView.fxml | 6 +- .../compare/ComparisonDialogDemo.java | 16 +++--- 4 files changed, 63 insertions(+), 64 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 779c58823e..e83c00911b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -6,10 +6,12 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import org.epics.vtype.VType; import org.phoebus.core.vtypes.VDisconnectedData; -import org.phoebus.saveandrestore.util.Utilities; +/** + * Data class for one column in the comparison table. + * @param + */ public class ColumnEntry { private final ObjectProperty snapshotVal = new SimpleObjectProperty<>(this, "snapshotValue", null); @@ -26,19 +28,7 @@ public ObjectProperty getSnapshotValue(){ public void setLiveVal(T value){ liveVal.set(value); - if(value instanceof String){ - String stringValue = (String)value; - delta.set(new ColumnDelta(stringValue, stringValue.equals(snapshotVal.get()))); - } - else{ - Double valueNumber = ((Number) value).doubleValue(); - Double diff = ((Number) snapshotVal.get()).doubleValue() - valueNumber; - String diffString = Double.toString(diff); - if(diff > 0){ - diffString = "+" + diff; - } - delta.set(new ColumnDelta(diffString, diff == 0)); - } + delta.set(new ColumnDelta()); } public ObjectProperty getLiveValue(){ @@ -49,13 +39,34 @@ public ObjectProperty getDelta(){ return delta; } - public static class ColumnDelta{ - private String deltaString; - private boolean equal; + /** + * Class wrapping data needed to render or sort the delta column. + */ + public class ColumnDelta{ + private final boolean equal; + private String displayString; + private final double absoluteDelta; + + public ColumnDelta(){ + if(liveVal.get() instanceof String){ + String stringValue = (String)liveVal.get(); + displayString = (String)snapshotVal.get(); + absoluteDelta = Math.abs((displayString).compareTo(stringValue)); + } + else{ + double valueNumber = ((Number) liveVal.get()).doubleValue(); + double diff = ((Number) snapshotVal.get()).doubleValue() - valueNumber; + displayString = Double.toString(diff); + if(diff > 0){ + displayString = "+" + diff; + } + absoluteDelta = Math.abs(diff); + } + equal = absoluteDelta == 0; + } - public ColumnDelta(String deltaString, boolean equal){ - this.deltaString = deltaString; - this.equal = equal; + public double getAbsoluteDelta(){ + return absoluteDelta; } public boolean isEqual() { @@ -64,7 +75,7 @@ public boolean isEqual() { @Override public String toString(){ - return deltaString; + return displayString; } } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 1f3c951065..fff212b13c 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -29,9 +29,9 @@ import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; -import org.phoebus.saveandrestore.util.Utilities; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -43,6 +43,10 @@ public class TableComparisonViewController { @FXML private TableView comparisonTable; + @SuppressWarnings("unused") + @FXML + private TableColumn indexColumn; + @SuppressWarnings("unused") @FXML private TableColumn storedValueColumn; @@ -77,20 +81,19 @@ public void initialize() { cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getLiveValue()); deltaColumn.setCellValueFactory(cell -> cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getDelta()); + deltaColumn.setComparator(Comparator.comparingDouble(ColumnEntry.ColumnDelta::getAbsoluteDelta)); - deltaColumn.setCellFactory(e -> new TableCell<>(){ + deltaColumn.setCellFactory(e -> new TableCell<>() { @Override public void updateItem(ColumnEntry.ColumnDelta item, boolean empty) { - if(item != null && !empty){ - if(!item.isEqual()){ + if (item != null && !empty) { + if (!item.isEqual()) { setStyle(TableCellColors.ALARM_MAJOR_STYLE); - } - else{ + } else { setStyle(TableCellColors.REGULAR_CELL_STYLE); } setText(item.toString()); } - } }); } @@ -145,8 +148,7 @@ public void loadDataAndConnect(VType data, String pvName) { index++; } } - } - else if (data instanceof VBooleanArray) { + } else if (data instanceof VBooleanArray) { ListBoolean listBoolean = ((VBooleanArray) data).getData(); for (int i = 0; i < listBoolean.size(); i++) { boolean value = listBoolean.getBoolean(i); @@ -188,45 +190,33 @@ private void updateTable(VType liveData) { comparisonTable.getItems().forEach(i -> { int index = i.indexProperty().get(); ColumnEntry columnEntry = i.getColumnEntries().get(index); - if (liveData instanceof VDoubleArray) { - VDoubleArray array = (VDoubleArray) liveData; - double value = array.getData().getDouble(index); - columnEntry.setLiveVal(value); - double absoluteDelta = (Double)columnEntry.getSnapshotValue().get() - value; - String deltaString = (absoluteDelta > 0 ? "+" : "-") + absoluteDelta; - //columnEntry.setDelta(new Utilities.VTypeComparison(deltaString, absoluteDelta > 0 ? 1 : -1, false, absoluteDelta)); - } /*else if (liveData instanceof VIntArray) { + if (liveData instanceof VDoubleArray array) { + columnEntry.setLiveVal(array.getData().getDouble(index)); + } else if (liveData instanceof VIntArray) { VIntArray array = (VIntArray) liveData; - int value = array.getData().getInt(index); - columnEntry.setLiveVal(value); - columnEntry.setDelta(((Integer)columnEntry.getSnapshotValue().get()) - value); + columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VLongArray) { VLongArray array = (VLongArray) liveData; long value = array.getData().getLong(index); - columnEntry.setLiveVal(value); - columnEntry.setDelta(((Long)columnEntry.getSnapshotValue().get()) - value); + columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VFloatArray) { VFloatArray array = (VFloatArray) liveData; float value = array.getData().getFloat(index); - columnEntry.setLiveVal(value); - columnEntry.setDelta(((Float)columnEntry.getSnapshotValue().get()) - value); + columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VShortArray) { VShortArray array = (VShortArray) liveData; short value = array.getData().getShort(index); - columnEntry.setLiveVal(value); - columnEntry.setDelta(((Short)columnEntry.getSnapshotValue().get()) - value); + columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VBooleanArray) { VBooleanArray array = (VBooleanArray)liveData; - boolean value = array.getData().getBoolean(index); - columnEntry.setLiveVal(value); - //columnEntry.setDelta(((Boolean)columnEntry.getSnapshotValue().get()) - value); + columnEntry.setLiveVal(array.getData().getBoolean(index)); } else if (liveData instanceof VEnumArray) { VEnumArray array = (VEnumArray) liveData; i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); } else if (liveData instanceof VStringArray) { VStringArray array = (VStringArray) liveData; i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); - }*/ + } }); } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index 8f6efe2cc6..bc723e2414 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -16,9 +16,9 @@ - - - + + + diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 33fc4730a6..8fdf54e609 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -5,30 +5,28 @@ package org.phoebus.applications.saveandrestore.ui.snapshot.compare; import javafx.stage.Stage; -import org.epics.util.array.ArrayBoolean; import org.epics.util.array.ArrayDouble; -import org.epics.util.array.ListDouble; import org.epics.vtype.Alarm; import org.epics.vtype.Display; import org.epics.vtype.Time; -import org.epics.vtype.VBooleanArray; import org.epics.vtype.VDoubleArray; -import org.epics.vtype.VIntArray; -import org.phoebus.pv.PVFactory; -import org.phoebus.pv.PVPool; import org.phoebus.ui.javafx.ApplicationWrapper; +/** + * Utility class for the purpose of testing the {@link ComparisonDialog}. It uses + * a local array data source for comparison to a hard coded {@link VDoubleArray}. + */ public class ComparisonDialogDemo extends ApplicationWrapper { - public static void main(String[] args){ + public static void main(String[] args) { launch(ComparisonDialogDemo.class, args); } @Override - public void start(Stage primaryStage) throws Exception { + public void start(Stage primaryStage) { VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(1, 2,3, 4, 5), + VDoubleArray.of(ArrayDouble.of(1, 2, 3, 4, 5), Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = From 6c6eef481b53d12676d2e00833362501044ac3c8 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 3 Dec 2025 13:55:30 +0100 Subject: [PATCH 09/23] Clickable delta cell to launch dialog if arrays are not equal --- .../applications/saveandrestore/Messages.java | 1 + .../ui/snapshot/VDeltaCellEditor.java | 37 +++++++++++++++---- .../TableComparisonViewController.java | 3 -- .../saveandrestore/messages.properties | 1 + 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index 8dd7ac7039..38a8034493 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -32,6 +32,7 @@ public class Messages { public static String buttonSearch; public static String cannotCompareHeader; public static String cannotCompareTitle; + public static String clickToCompare; public static String closeConfigurationWarning; public static String closeCompositeSnapshotWarning; public static String closeSnapshotWarning; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java index a3a8d0ead3..5aeb4d7f13 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java @@ -20,10 +20,17 @@ package org.phoebus.applications.saveandrestore.ui.snapshot; import javafx.scene.control.Tooltip; +import org.epics.vtype.VEnumArray; +import org.epics.vtype.VNumberArray; +import org.epics.vtype.VStringArray; +import org.epics.vtype.VTable; +import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.ui.VTypePair; +import org.phoebus.applications.saveandrestore.ui.snapshot.compare.ComparisonDialog; import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.saveandrestore.util.Utilities; import org.phoebus.saveandrestore.util.VNoData; +import org.phoebus.ui.dialog.DialogHelper; import java.util.Formatter; @@ -74,15 +81,29 @@ public void updateItem(T item, boolean empty) { setText(pair.value.toString()); } else { Utilities.VTypeComparison vtc = Utilities.deltaValueToString(pair.value, pair.base, pair.threshold); - String percentage = Utilities.deltaValueToPercentage(pair.value, pair.base); - if (!percentage.isEmpty() && showDeltaPercentage) { - Formatter formatter = new Formatter(); - setText(formatter.format("%g", Double.parseDouble(vtc.getString())) + " (" + percentage + ")"); - } else { - setText(vtc.getString()); - } - if (!vtc.isWithinThreshold()) { + if (vtc.getValuesEqual() != 0 && + (pair.base instanceof VNumberArray || + pair.base instanceof VStringArray || + pair.base instanceof VEnumArray)) { + TableEntry tableEntry = getTableRow().getItem(); + setText(Messages.clickToCompare); setStyle(TableCellColors.ALARM_MAJOR_STYLE); + setOnMouseClicked(e -> { + ComparisonDialog comparisonDialog = new ComparisonDialog(tableEntry.getSnapshotVal().get(), tableEntry.getConfigPv().getPvName()); + DialogHelper.positionDialog(comparisonDialog, getTableView(), -400, -400); + comparisonDialog.show(); + }); + } else { + String percentage = Utilities.deltaValueToPercentage(pair.value, pair.base); + if (!percentage.isEmpty() && showDeltaPercentage) { + Formatter formatter = new Formatter(); + setText(formatter.format("%g", Double.parseDouble(vtc.getString())) + " (" + percentage + ")"); + } else { + setText(vtc.getString()); + } + if (!vtc.isWithinThreshold()) { + setStyle(TableCellColors.ALARM_MAJOR_STYLE); + } } } tooltip.setText(item.toString()); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index fff212b13c..d201ae3b15 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -197,15 +197,12 @@ private void updateTable(VType liveData) { columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VLongArray) { VLongArray array = (VLongArray) liveData; - long value = array.getData().getLong(index); columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VFloatArray) { VFloatArray array = (VFloatArray) liveData; - float value = array.getData().getFloat(index); columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VShortArray) { VShortArray array = (VShortArray) liveData; - short value = array.getData().getShort(index); columnEntry.setLiveVal(array.getData().getDouble(index)); } else if (liveData instanceof VBooleanArray) { VBooleanArray array = (VBooleanArray)liveData; diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 6d19e32639..7070d4f03f 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -15,6 +15,7 @@ cancel=Cancel cannotCompareHeader=No snapshot data available for comparison. cannotCompareTitle=Cannot Compare choose=Choose +clickToCompare=Click to compare closeConfigurationWarning=Save&restore configuration modified, but not saved. Do you wish to continue? closeCompositeSnapshotWarning=Composite snapshot modified, but not saved. Do you wish to continue? closeSnapshotWarning=Save&restore snapshot created or modified, but not saved. Do you wish to continue? From 7141bd3222d3c1299353ddc37ba0bba7de56916b Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 4 Dec 2025 13:26:26 +0100 Subject: [PATCH 10/23] Using VTypes instead of priminitives for the comparison dialog --- .../saveandrestore/ui/VTypePair.java | 27 ++++++ .../ui/snapshot/SnapshotController.java | 4 +- .../ui/snapshot/VDeltaCellEditor.java | 11 ++- .../ui/snapshot/VTypeCellEditor.java | 4 +- .../ui/snapshot/compare/ColumnEntry.java | 87 +++++++------------ .../TableComparisonViewController.java | 80 +++++++++-------- 6 files changed, 110 insertions(+), 103 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java index 1d71acd33b..c792272786 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java @@ -17,6 +17,8 @@ */ package org.phoebus.applications.saveandrestore.ui; +import org.epics.vtype.VNumber; +import org.epics.vtype.VString; import org.epics.vtype.VType; import org.phoebus.saveandrestore.util.Threshold; @@ -47,6 +49,31 @@ public VTypePair(VType base, VType value, Optional> threshold) { this.threshold = threshold; } + /** + * Computes absolute delta for the delta between {@link #base} and {@link #value}. When applied to + * {@link VString} types, {@link String#compareTo(String)} is used for comparison, but then converted to + * absolute value to. + * + *

+ * Main use case for this is ordering on delta. Absolute delta is more useful as otherwise zero + * deltas would be found between positive and negative deltas. + *

+ * @return + */ + public double getAbsoluteDelta(){ + if(base == null || value == null){ + return 0.0; + } + if(base instanceof VNumber){ + return Math.abs(((VNumber)base).getValue().doubleValue() - + ((VNumber)value).getValue().doubleValue()); + } + else if(base instanceof VString){ + return Math.abs(((VString)base).getValue().compareTo(((VString)value).getValue())); + } + else return 0.0; + } + /* * (non-Javadoc) * diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index 6c7eaca1f6..0e5781836f 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -645,7 +645,7 @@ protected void updateItem(TableEntry item, boolean empty) { }); showDeltaPercentage.addListener((ob, o, n) -> deltaColumn.setCellFactory(e -> { - VDeltaCellEditor vDeltaCellEditor = new VDeltaCellEditor<>(); + VDeltaCellEditor vDeltaCellEditor = new VDeltaCellEditor<>(); vDeltaCellEditor.setShowDeltaPercentage(n); return vDeltaCellEditor; })); @@ -1453,7 +1453,7 @@ private void addSnapshot(Snapshot snapshot) { "", minWidth); deltaCol.setCellValueFactory(e -> e.getValue().compareValueProperty(additionalSnapshots.size())); deltaCol.setCellFactory(e -> { - VDeltaCellEditor vDeltaCellEditor = new VDeltaCellEditor<>(); + VDeltaCellEditor vDeltaCellEditor = new VDeltaCellEditor<>(); vDeltaCellEditor.setShowDeltaPercentage(showDeltaPercentage.get()); return vDeltaCellEditor; }); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java index 5aeb4d7f13..07e5840430 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java @@ -23,7 +23,6 @@ import org.epics.vtype.VEnumArray; import org.epics.vtype.VNumberArray; import org.epics.vtype.VStringArray; -import org.epics.vtype.VTable; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.applications.saveandrestore.ui.snapshot.compare.ComparisonDialog; @@ -41,7 +40,7 @@ * @param * @author Kunal Shroff */ -public class VDeltaCellEditor extends VTypeCellEditor { +public class VDeltaCellEditor extends VTypeCellEditor { private final Tooltip tooltip = new Tooltip(); @@ -51,7 +50,7 @@ protected void setShowDeltaPercentage(boolean showDeltaPercentage) { this.showDeltaPercentage = showDeltaPercentage; } - VDeltaCellEditor() { + public VDeltaCellEditor() { super(); } @@ -83,9 +82,9 @@ public void updateItem(T item, boolean empty) { Utilities.VTypeComparison vtc = Utilities.deltaValueToString(pair.value, pair.base, pair.threshold); if (vtc.getValuesEqual() != 0 && (pair.base instanceof VNumberArray || - pair.base instanceof VStringArray || - pair.base instanceof VEnumArray)) { - TableEntry tableEntry = getTableRow().getItem(); + pair.base instanceof VStringArray || + pair.base instanceof VEnumArray)) { + TableEntry tableEntry = (TableEntry) getTableRow().getItem(); setText(Messages.clickToCompare); setStyle(TableCellColors.ALARM_MAJOR_STYLE); setOnMouseClicked(e -> { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VTypeCellEditor.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VTypeCellEditor.java index 032676186e..ff10c3a32e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VTypeCellEditor.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VTypeCellEditor.java @@ -49,10 +49,10 @@ * @param {@link org.epics.vtype.VType} or {@link org.phoebus.applications.saveandrestore.ui.VTypePair} * @author Jaka Bobnar */ -public class VTypeCellEditor extends MultitypeTableCell { +public class VTypeCellEditor extends MultitypeTableCell { private final Tooltip tooltip = new Tooltip(); - VTypeCellEditor() { + public VTypeCellEditor() { setConverter(new StringConverter<>() { @Override diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index e83c00911b..2745aeb44e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -6,76 +6,53 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.saveandrestore.util.Threshold; +import org.phoebus.saveandrestore.util.VNoData; + +import java.util.Optional; /** * Data class for one column in the comparison table. - * @param */ -public class ColumnEntry { +public class ColumnEntry { - private final ObjectProperty snapshotVal = new SimpleObjectProperty<>(this, "snapshotValue", null); - private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); - private final ObjectProperty liveVal = new SimpleObjectProperty<>(this, "liveValue", (T) VDisconnectedData.INSTANCE); + /** + * The {@link VType} value as stored in a {@link org.phoebus.applications.saveandrestore.model.Snapshot} + */ + private final ObjectProperty storedValue = new SimpleObjectProperty<>(this, "storedValue", null); + /** + * A {@link VTypePair} property holding data for the purpose of calculating and showing a delta. + */ + private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); + /** + * The libe {@link VType} value as read from a connected PV. + */ + private final ObjectProperty liveValue = new SimpleObjectProperty<>(this, "liveValue", VNoData.INSTANCE); - public ColumnEntry(T snapshotVal){ - this.snapshotVal.set(snapshotVal); - } + private Optional> threshold = Optional.empty(); - public ObjectProperty getSnapshotValue(){ - return snapshotVal; + public ColumnEntry(VType storedValue) { + this.storedValue.set(storedValue); } - public void setLiveVal(T value){ - liveVal.set(value); - delta.set(new ColumnDelta()); + public ObjectProperty storedValueProperty() { + return storedValue; } - public ObjectProperty getLiveValue(){ - return liveVal; + public void setLiveVal(VType value) { + liveValue.set(value); + VTypePair vTypePair = new VTypePair(storedValue.get(), value, threshold); + delta.set(vTypePair); } - public ObjectProperty getDelta(){ - return delta; + public ObjectProperty liveValueProperty() { + return liveValue; } - /** - * Class wrapping data needed to render or sort the delta column. - */ - public class ColumnDelta{ - private final boolean equal; - private String displayString; - private final double absoluteDelta; - - public ColumnDelta(){ - if(liveVal.get() instanceof String){ - String stringValue = (String)liveVal.get(); - displayString = (String)snapshotVal.get(); - absoluteDelta = Math.abs((displayString).compareTo(stringValue)); - } - else{ - double valueNumber = ((Number) liveVal.get()).doubleValue(); - double diff = ((Number) snapshotVal.get()).doubleValue() - valueNumber; - displayString = Double.toString(diff); - if(diff > 0){ - displayString = "+" + diff; - } - absoluteDelta = Math.abs(diff); - } - equal = absoluteDelta == 0; - } - - public double getAbsoluteDelta(){ - return absoluteDelta; - } - - public boolean isEqual() { - return equal; - } - - @Override - public String toString(){ - return displayString; - } + public ObjectProperty getDelta() { + return delta; } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index d201ae3b15..2655df48f1 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -5,27 +5,41 @@ package org.phoebus.applications.saveandrestore.ui.snapshot.compare; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; -import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import org.epics.util.array.IteratorNumber; import org.epics.util.array.ListBoolean; +import org.epics.vtype.Alarm; +import org.epics.vtype.Display; +import org.epics.vtype.Time; +import org.epics.vtype.VBoolean; import org.epics.vtype.VBooleanArray; +import org.epics.vtype.VByte; import org.epics.vtype.VByteArray; +import org.epics.vtype.VDouble; import org.epics.vtype.VDoubleArray; +import org.epics.vtype.VEnum; import org.epics.vtype.VEnumArray; +import org.epics.vtype.VFloat; import org.epics.vtype.VFloatArray; +import org.epics.vtype.VInt; import org.epics.vtype.VIntArray; +import org.epics.vtype.VLong; import org.epics.vtype.VLongArray; import org.epics.vtype.VNumberArray; +import org.epics.vtype.VShort; import org.epics.vtype.VShortArray; +import org.epics.vtype.VString; import org.epics.vtype.VStringArray; import org.epics.vtype.VType; -import org.phoebus.applications.saveandrestore.ui.snapshot.TableCellColors; +import org.phoebus.applications.saveandrestore.ui.VTypePair; +import org.phoebus.applications.saveandrestore.ui.snapshot.VDeltaCellEditor; +import org.phoebus.applications.saveandrestore.ui.snapshot.VTypeCellEditor; import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; @@ -49,16 +63,16 @@ public class TableComparisonViewController { @SuppressWarnings("unused") @FXML - private TableColumn storedValueColumn; + private TableColumn storedValueColumn; @SuppressWarnings("unused") @FXML - private TableColumn liveValueColumn; + private TableColumn liveValueColumn; @SuppressWarnings("unused") @FXML - private TableColumn deltaColumn; + private TableColumn deltaColumn; @SuppressWarnings("unused") @FXML @@ -76,26 +90,16 @@ public void initialize() { comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); pvName.textProperty().bind(pvNameProperty); storedValueColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getSnapshotValue()); + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).storedValueProperty()); + storedValueColumn.setCellFactory(e -> new VTypeCellEditor<>()); liveValueColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getLiveValue()); + cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).liveValueProperty()); + liveValueColumn.setCellFactory(e -> new VTypeCellEditor<>()); deltaColumn.setCellValueFactory(cell -> cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getDelta()); - deltaColumn.setComparator(Comparator.comparingDouble(ColumnEntry.ColumnDelta::getAbsoluteDelta)); - - deltaColumn.setCellFactory(e -> new TableCell<>() { - @Override - public void updateItem(ColumnEntry.ColumnDelta item, boolean empty) { - if (item != null && !empty) { - if (!item.isEqual()) { - setStyle(TableCellColors.ALARM_MAJOR_STYLE); - } else { - setStyle(TableCellColors.REGULAR_CELL_STYLE); - } - setText(item.toString()); - } - } - }); + deltaColumn.setComparator(Comparator.comparingDouble(VTypePair::getAbsoluteDelta)); + + deltaColumn.setCellFactory(e -> new VDeltaCellEditor<>()); } public void loadDataAndConnect(VType data, String pvName) { @@ -108,42 +112,42 @@ public void loadDataAndConnect(VType data, String pvName) { if (data instanceof VDoubleArray) { while (iteratorNumber.hasNext()) { double value = iteratorNumber.nextDouble(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } } else if (data instanceof VFloatArray) { while (iteratorNumber.hasNext()) { float value = iteratorNumber.nextFloat(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } } else if (data instanceof VIntArray) { while (iteratorNumber.hasNext()) { int value = iteratorNumber.nextInt(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } } else if (data instanceof VLongArray) { while (iteratorNumber.hasNext()) { long value = iteratorNumber.nextLong(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } } else if (data instanceof VShortArray) { while (iteratorNumber.hasNext()) { short value = iteratorNumber.nextShort(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } } else if (data instanceof VByteArray) { while (iteratorNumber.hasNext()) { byte value = iteratorNumber.nextByte(); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, Alarm.none(), Time.now(), Display.none())); addRow(index, columnEntries, columnEntry); index++; } @@ -152,13 +156,13 @@ public void loadDataAndConnect(VType data, String pvName) { ListBoolean listBoolean = ((VBooleanArray) data).getData(); for (int i = 0; i < listBoolean.size(); i++) { boolean value = listBoolean.getBoolean(i); - ColumnEntry columnEntry = new ColumnEntry<>(value); + ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, Alarm.none(), Time.now())); addRow(i, columnEntries, columnEntry); } } else if (data instanceof VEnumArray) { List enumValues = ((VEnumArray) data).getData(); for (int i = 0; i < enumValues.size(); i++) { - ColumnEntry columnEntry = new ColumnEntry<>(enumValues.get(i)); + ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(i), Alarm.none(), Time.now())); addRow(i, columnEntries, columnEntry); } } @@ -191,28 +195,28 @@ private void updateTable(VType liveData) { int index = i.indexProperty().get(); ColumnEntry columnEntry = i.getColumnEntries().get(index); if (liveData instanceof VDoubleArray array) { - columnEntry.setLiveVal(array.getData().getDouble(index)); + columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), Alarm.none(), Time.now(), Display.none())); } else if (liveData instanceof VIntArray) { VIntArray array = (VIntArray) liveData; - columnEntry.setLiveVal(array.getData().getDouble(index)); + columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), Alarm.none(), Time.now(), Display.none())); } else if (liveData instanceof VLongArray) { VLongArray array = (VLongArray) liveData; - columnEntry.setLiveVal(array.getData().getDouble(index)); + columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), Alarm.none(), Time.now(), Display.none())); } else if (liveData instanceof VFloatArray) { VFloatArray array = (VFloatArray) liveData; - columnEntry.setLiveVal(array.getData().getDouble(index)); + columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), Alarm.none(), Time.now(), Display.none())); } else if (liveData instanceof VShortArray) { VShortArray array = (VShortArray) liveData; - columnEntry.setLiveVal(array.getData().getDouble(index)); + columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), Alarm.none(), Time.now(), Display.none())); } else if (liveData instanceof VBooleanArray) { VBooleanArray array = (VBooleanArray)liveData; - columnEntry.setLiveVal(array.getData().getBoolean(index)); + columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), Alarm.none(), Time.now())); } else if (liveData instanceof VEnumArray) { VEnumArray array = (VEnumArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } else if (liveData instanceof VStringArray) { VStringArray array = (VStringArray) liveData; - i.getColumnEntries().get(index).setLiveVal(array.getData().get(index)); + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } }); } From 0f70bee1dbad41fd2d281711c577f985fe302b7e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 4 Dec 2025 13:50:43 +0100 Subject: [PATCH 11/23] Handle update if live data has fewer elements than stored snapshot --- .../TableComparisonViewController.java | 42 ++++++++++++------- .../compare/ComparisonDialogDemo.java | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 2655df48f1..d0eba16a14 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -43,6 +43,7 @@ import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; +import org.phoebus.saveandrestore.util.VNoData; import java.util.ArrayList; import java.util.Comparator; @@ -194,28 +195,41 @@ private void updateTable(VType liveData) { comparisonTable.getItems().forEach(i -> { int index = i.indexProperty().get(); ColumnEntry columnEntry = i.getColumnEntries().get(index); - if (liveData instanceof VDoubleArray array) { - columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VIntArray) { - VIntArray array = (VIntArray) liveData; - columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VLongArray) { - VLongArray array = (VLongArray) liveData; - columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VFloatArray) { - VFloatArray array = (VFloatArray) liveData; - columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VShortArray) { - VShortArray array = (VShortArray) liveData; - columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), Alarm.none(), Time.now(), Display.none())); + if(liveData instanceof VNumberArray){ + if(index >= ((VNumberArray)liveData).getData().size()){ + columnEntry.setLiveVal(VNoData.INSTANCE); + } else if (liveData instanceof VDoubleArray array) { + columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), Alarm.none(), Time.now(), Display.none())); + } else if (liveData instanceof VIntArray) { + VIntArray array = (VIntArray) liveData; + columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), Alarm.none(), Time.now(), Display.none())); + } else if (liveData instanceof VLongArray) { + VLongArray array = (VLongArray) liveData; + columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), Alarm.none(), Time.now(), Display.none())); + } else if (liveData instanceof VFloatArray) { + VFloatArray array = (VFloatArray) liveData; + columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), Alarm.none(), Time.now(), Display.none())); + } else if (liveData instanceof VShortArray) { + VShortArray array = (VShortArray) liveData; + columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), Alarm.none(), Time.now(), Display.none())); + } } else if (liveData instanceof VBooleanArray) { VBooleanArray array = (VBooleanArray)liveData; + if(index > array.getData().size()){ + columnEntry.setLiveVal(VNoData.INSTANCE); + } columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), Alarm.none(), Time.now())); } else if (liveData instanceof VEnumArray) { VEnumArray array = (VEnumArray) liveData; + if(index > array.getData().size()){ + columnEntry.setLiveVal(VNoData.INSTANCE); + } i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } else if (liveData instanceof VStringArray) { VStringArray array = (VStringArray) liveData; + if(index > array.getData().size()){ + columnEntry.setLiveVal(VNoData.INSTANCE); + } i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } }); diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 8fdf54e609..94d25dd2bd 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -30,7 +30,7 @@ public void start(Stage primaryStage) { Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(1, 8, 7, 0, -1)"); + new ComparisonDialog(vDoubleArray, "loc://x(1, 8, 7, 0)"); comparisonDialog.show(); } } From b97b54d6572e0cc34c861360aff6caba584756ef Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 5 Dec 2025 11:24:45 +0100 Subject: [PATCH 12/23] Proper handling of differences in array length in comparisoon dialog --- .../saveandrestore/ui/VTypePair.java | 9 +- .../ui/snapshot/compare/ColumnEntry.java | 11 +- .../TableComparisonViewController.java | 228 ++++++++++++------ 3 files changed, 165 insertions(+), 83 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java index c792272786..d653876b52 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java @@ -37,7 +37,8 @@ public class VTypePair { public final Optional> threshold; /** - * Constructs a new pair. + * Constructs a new pair. In the context of save-and-restore snapshots, the {@link #value} field + * is used to hold a stored value, while {@link #base} holds the live PV value. * * @param base the base value * @param value the value that can be compared to base @@ -52,13 +53,13 @@ public VTypePair(VType base, VType value, Optional> threshold) { /** * Computes absolute delta for the delta between {@link #base} and {@link #value}. When applied to * {@link VString} types, {@link String#compareTo(String)} is used for comparison, but then converted to - * absolute value to. + * an absolute value. * *

- * Main use case for this is ordering on delta. Absolute delta is more useful as otherwise zero + * Main use case for this is ordering on delta. Absolute delta may be more useful as otherwise zero * deltas would be found between positive and negative deltas. *

- * @return + * @return Absolute delta between {@link #base} and {@link #value}. */ public double getAbsoluteDelta(){ if(base == null || value == null){ diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 2745aeb44e..013356a54e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -8,7 +8,6 @@ import javafx.beans.property.SimpleObjectProperty; import org.epics.vtype.VType; import org.phoebus.applications.saveandrestore.ui.VTypePair; -import org.phoebus.core.vtypes.VDisconnectedData; import org.phoebus.saveandrestore.util.Threshold; import org.phoebus.saveandrestore.util.VNoData; @@ -28,11 +27,11 @@ public class ColumnEntry { */ private final ObjectProperty delta = new SimpleObjectProperty<>(this, "delta", null); /** - * The libe {@link VType} value as read from a connected PV. + * The live {@link VType} value as read from a connected PV. */ private final ObjectProperty liveValue = new SimpleObjectProperty<>(this, "liveValue", VNoData.INSTANCE); - private Optional> threshold = Optional.empty(); + private final Optional> threshold = Optional.empty(); public ColumnEntry(VType storedValue) { this.storedValue.set(storedValue); @@ -42,9 +41,9 @@ public ObjectProperty storedValueProperty() { return storedValue; } - public void setLiveVal(VType value) { - liveValue.set(value); - VTypePair vTypePair = new VTypePair(storedValue.get(), value, threshold); + public void setLiveVal(VType liveValue) { + this.liveValue.set(liveValue); + VTypePair vTypePair = new VTypePair(liveValue, storedValue.get(), threshold); delta.set(vTypePair); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index d0eba16a14..0940090242 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -5,14 +5,12 @@ package org.phoebus.applications.saveandrestore.ui.snapshot.compare; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import org.epics.util.array.IteratorNumber; import org.epics.util.array.ListBoolean; import org.epics.vtype.Alarm; import org.epics.vtype.Display; @@ -23,7 +21,6 @@ import org.epics.vtype.VByteArray; import org.epics.vtype.VDouble; import org.epics.vtype.VDoubleArray; -import org.epics.vtype.VEnum; import org.epics.vtype.VEnumArray; import org.epics.vtype.VFloat; import org.epics.vtype.VFloatArray; @@ -49,6 +46,7 @@ import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -91,13 +89,13 @@ public void initialize() { comparisonTable.getStylesheets().add(TableComparisonViewController.class.getResource("/save-and-restore-style.css").toExternalForm()); pvName.textProperty().bind(pvNameProperty); storedValueColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).storedValueProperty()); + cell.getValue().getColumnEntries().get(0).storedValueProperty()); storedValueColumn.setCellFactory(e -> new VTypeCellEditor<>()); liveValueColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).liveValueProperty()); + cell.getValue().getColumnEntries().get(0).liveValueProperty()); liveValueColumn.setCellFactory(e -> new VTypeCellEditor<>()); deltaColumn.setCellValueFactory(cell -> - cell.getValue().getColumnEntries().get(cell.getValue().indexProperty().get()).getDelta()); + cell.getValue().getColumnEntries().get(0).getDelta()); deltaColumn.setComparator(Comparator.comparingDouble(VTypePair::getAbsoluteDelta)); deltaColumn.setCellFactory(e -> new VDeltaCellEditor<>()); @@ -106,65 +104,76 @@ public void initialize() { public void loadDataAndConnect(VType data, String pvName) { pvNameProperty.set(pvName); - List columnEntries = new ArrayList<>(); + if (data instanceof VNumberArray) { - int index = 0; - IteratorNumber iteratorNumber = ((VNumberArray) data).getData().iterator(); - if (data instanceof VDoubleArray) { - while (iteratorNumber.hasNext()) { - double value = iteratorNumber.nextDouble(); + int arraySize = ((VNumberArray) data).getData().size(); + for (int index = 0; index < arraySize; index++) { + List columnEntries = new ArrayList<>(); + if (data instanceof VDoubleArray) { + double value = ((VDoubleArray) data).getData().getDouble(index); ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; - } - } else if (data instanceof VFloatArray) { - while (iteratorNumber.hasNext()) { - float value = iteratorNumber.nextFloat(); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } else if (data instanceof VFloatArray) { + float value = ((VFloatArray) data).getData().getFloat(index); ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; - } - } else if (data instanceof VIntArray) { - while (iteratorNumber.hasNext()) { - int value = iteratorNumber.nextInt(); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } else if (data instanceof VIntArray) { + int value = ((VIntArray) data).getData().getInt(index); ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; - } - } else if (data instanceof VLongArray) { - while (iteratorNumber.hasNext()) { - long value = iteratorNumber.nextLong(); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } else if (data instanceof VLongArray) { + long value = ((VLongArray) data).getData().getLong(index); ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; - } - } else if (data instanceof VShortArray) { - while (iteratorNumber.hasNext()) { - short value = iteratorNumber.nextShort(); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } else if (data instanceof VShortArray) { + short value = ((VShortArray) data).getData().getShort(index); ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; - } - } else if (data instanceof VByteArray) { - while (iteratorNumber.hasNext()) { - byte value = iteratorNumber.nextByte(); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } else if (data instanceof VByteArray) { + byte value = ((VByteArray) data).getData().getByte(index); ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, Alarm.none(), Time.now(), Display.none())); - addRow(index, columnEntries, columnEntry); - index++; + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); } } } else if (data instanceof VBooleanArray) { ListBoolean listBoolean = ((VBooleanArray) data).getData(); - for (int i = 0; i < listBoolean.size(); i++) { - boolean value = listBoolean.getBoolean(i); + for (int index = 0; index < listBoolean.size(); index++) { + List columnEntries = new ArrayList<>(); + boolean value = listBoolean.getBoolean(index); ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, Alarm.none(), Time.now())); - addRow(i, columnEntries, columnEntry); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); } } else if (data instanceof VEnumArray) { List enumValues = ((VEnumArray) data).getData(); - for (int i = 0; i < enumValues.size(); i++) { - ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(i), Alarm.none(), Time.now())); - addRow(i, columnEntries, columnEntry); + for (int index = 0; index < enumValues.size(); index++) { + List columnEntries = new ArrayList<>(); + ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(index), Alarm.none(), Time.now())); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); + } + } else if (data instanceof VStringArray) { + List stringValues = ((VStringArray) data).getData(); + for (int index = 0; index < stringValues.size(); index++) { + List columnEntries = new ArrayList<>(); + ColumnEntry columnEntry = new ColumnEntry(VString.of(stringValues.get(index), Alarm.none(), Time.now())); + columnEntries.add(columnEntry); + ComparisonData comparisonData = new ComparisonData(index, columnEntries); + comparisonTable.getItems().add(index, comparisonData); } } @@ -192,47 +201,120 @@ private void updateTable(VType liveData) { if (liveData.equals(VDisconnectedData.INSTANCE)) { comparisonTable.getItems().forEach(i -> i.getColumnEntries().get(0).setLiveVal(VDisconnectedData.INSTANCE)); } else { + AtomicInteger liveDataArraySize = new AtomicInteger(0); comparisonTable.getItems().forEach(i -> { int index = i.indexProperty().get(); - ColumnEntry columnEntry = i.getColumnEntries().get(index); - if(liveData instanceof VNumberArray){ - if(index >= ((VNumberArray)liveData).getData().size()){ + ColumnEntry columnEntry = i.getColumnEntries().get(0); + if (liveData instanceof VNumberArray) { + liveDataArraySize.set(((VNumberArray) liveData).getData().size()); + if (index >= liveDataArraySize.get()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else if (liveData instanceof VDoubleArray array) { columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VIntArray) { - VIntArray array = (VIntArray) liveData; + } else if (liveData instanceof VIntArray array) { columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VLongArray) { - VLongArray array = (VLongArray) liveData; + } else if (liveData instanceof VLongArray array) { columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VFloatArray) { - VFloatArray array = (VFloatArray) liveData; + } else if (liveData instanceof VFloatArray array) { columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), Alarm.none(), Time.now(), Display.none())); - } else if (liveData instanceof VShortArray) { - VShortArray array = (VShortArray) liveData; + } else if (liveData instanceof VShortArray array) { columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), Alarm.none(), Time.now(), Display.none())); } - } else if (liveData instanceof VBooleanArray) { - VBooleanArray array = (VBooleanArray)liveData; - if(index > array.getData().size()){ + } else if (liveData instanceof VBooleanArray array) { + liveDataArraySize.set(array.getData().size()); + if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); + } else { + columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), Alarm.none(), Time.now())); } - columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), Alarm.none(), Time.now())); - } else if (liveData instanceof VEnumArray) { - VEnumArray array = (VEnumArray) liveData; - if(index > array.getData().size()){ + + } else if (liveData instanceof VEnumArray array) { + liveDataArraySize.set(array.getData().size()); + if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); + } else { + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); - } else if (liveData instanceof VStringArray) { - VStringArray array = (VStringArray) liveData; - if(index > array.getData().size()){ + } else if (liveData instanceof VStringArray array) { + liveDataArraySize.set(array.getData().size()); + if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); + } else { + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); } }); + // Live data may have more elements than stored data + if (liveDataArraySize.get() > comparisonTable.getItems().size()) { + List columnEntries = new ArrayList<>(); + if (liveData instanceof VNumberArray) { + if (liveData instanceof VDoubleArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + double value = ((VDoubleArray) liveData).getData().getDouble(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } else if (liveData instanceof VFloatArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + float value = ((VFloatArray) liveData).getData().getFloat(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VFloat.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } else if (liveData instanceof VIntArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + int value = ((VIntArray) liveData).getData().getInt(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VInt.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } else if (liveData instanceof VLongArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + long value = ((VLongArray) liveData).getData().getLong(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VLong.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } else if (liveData instanceof VShortArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + short value = ((VShortArray) liveData).getData().getShort(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VShort.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } else if (liveData instanceof VByteArray) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + byte value = ((VByteArray) liveData).getData().getByte(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VByte.of(value, Alarm.none(), Time.now(), Display.none())); + addRow(index, columnEntries, columnEntry); + } + } + } else if (liveData instanceof VBooleanArray) { + ListBoolean listBoolean = ((VBooleanArray) liveData).getData(); + for (int i = 0; i < listBoolean.size(); i++) { + boolean value = listBoolean.getBoolean(i); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VBoolean.of(value, Alarm.none(), Time.now())); + addRow(i, columnEntries, columnEntry); + } + } else if (liveData instanceof VEnumArray) { + List enumValues = ((VEnumArray) liveData).getData(); + for (int i = 0; i < enumValues.size(); i++) { + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VString.of(enumValues.get(i), Alarm.none(), Time.now())); + addRow(i, columnEntries, columnEntry); + } + } else if (liveData instanceof VStringArray) { + List stringValues = ((VStringArray) liveData).getData(); + for (int i = 0; i < stringValues.size(); i++) { + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VString.of(stringValues.get(i), Alarm.none(), Time.now())); + addRow(i, columnEntries, columnEntry); + } + } + } } } } From 0024eede3e93175b4f64f6d6a099785f67121c58 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 5 Dec 2025 13:35:13 +0100 Subject: [PATCH 13/23] Minor layout and code cleanup changes --- .../TableComparisonViewController.java | 124 +++++++++--------- .../snapshot/compare/TableComparisonView.fxml | 2 +- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 0940090242..2ca880c62b 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -109,68 +109,68 @@ public void loadDataAndConnect(VType data, String pvName) { int arraySize = ((VNumberArray) data).getData().size(); for (int index = 0; index < arraySize; index++) { List columnEntries = new ArrayList<>(); - if (data instanceof VDoubleArray) { - double value = ((VDoubleArray) data).getData().getDouble(index); - ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); + if (data instanceof VDoubleArray array) { + double value = array.getData().getDouble(index); + ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); - } else if (data instanceof VFloatArray) { - float value = ((VFloatArray) data).getData().getFloat(index); - ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, Alarm.none(), Time.now(), Display.none())); + } else if (data instanceof VFloatArray array) { + float value = array.getData().getFloat(index); + ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); - } else if (data instanceof VIntArray) { - int value = ((VIntArray) data).getData().getInt(index); - ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, Alarm.none(), Time.now(), Display.none())); + } else if (data instanceof VIntArray array) { + int value = array.getData().getInt(index); + ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); - } else if (data instanceof VLongArray) { - long value = ((VLongArray) data).getData().getLong(index); - ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, Alarm.none(), Time.now(), Display.none())); + } else if (data instanceof VLongArray array) { + long value = array.getData().getLong(index); + ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); - } else if (data instanceof VShortArray) { - short value = ((VShortArray) data).getData().getShort(index); - ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, Alarm.none(), Time.now(), Display.none())); + } else if (data instanceof VShortArray array) { + short value = array.getData().getShort(index); + ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); - } else if (data instanceof VByteArray) { - byte value = ((VByteArray) data).getData().getByte(index); - ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, Alarm.none(), Time.now(), Display.none())); + } else if (data instanceof VByteArray array) { + byte value = array.getData().getByte(index); + ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); } } - } else if (data instanceof VBooleanArray) { - ListBoolean listBoolean = ((VBooleanArray) data).getData(); + } else if (data instanceof VBooleanArray array) { + ListBoolean listBoolean = array.getData(); for (int index = 0; index < listBoolean.size(); index++) { List columnEntries = new ArrayList<>(); boolean value = listBoolean.getBoolean(index); - ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, Alarm.none(), Time.now())); + ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, array.getAlarm(), array.getTime())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); } - } else if (data instanceof VEnumArray) { - List enumValues = ((VEnumArray) data).getData(); + } else if (data instanceof VEnumArray array) { + List enumValues = array.getData(); for (int index = 0; index < enumValues.size(); index++) { List columnEntries = new ArrayList<>(); - ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(index), Alarm.none(), Time.now())); + ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(index), array.getAlarm(), array.getTime())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); } - } else if (data instanceof VStringArray) { - List stringValues = ((VStringArray) data).getData(); + } else if (data instanceof VStringArray array) { + List stringValues = array.getData(); for (int index = 0; index < stringValues.size(); index++) { List columnEntries = new ArrayList<>(); - ColumnEntry columnEntry = new ColumnEntry(VString.of(stringValues.get(index), Alarm.none(), Time.now())); + ColumnEntry columnEntry = new ColumnEntry(VString.of(stringValues.get(index), array.getAlarm(), array.getTime())); columnEntries.add(columnEntry); ComparisonData comparisonData = new ComparisonData(index, columnEntries); comparisonTable.getItems().add(index, comparisonData); @@ -210,22 +210,22 @@ private void updateTable(VType liveData) { if (index >= liveDataArraySize.get()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else if (liveData instanceof VDoubleArray array) { - columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VIntArray array) { - columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VLongArray array) { - columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VFloatArray array) { - columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VShortArray array) { - columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VShort.of(array.getData().getShort(index),array.getAlarm(), array.getTime(), array.getDisplay())); } } else if (liveData instanceof VBooleanArray array) { liveDataArraySize.set(array.getData().size()); if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { - columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), Alarm.none(), Time.now())); + columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), array.getAlarm(), array.getTime())); } } else if (liveData instanceof VEnumArray array) { @@ -233,14 +233,14 @@ private void updateTable(VType liveData) { if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); } } else if (liveData instanceof VStringArray array) { liveDataArraySize.set(array.getData().size()); if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), Alarm.none(), Time.now())); + i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); } } }); @@ -248,69 +248,69 @@ private void updateTable(VType liveData) { if (liveDataArraySize.get() > comparisonTable.getItems().size()) { List columnEntries = new ArrayList<>(); if (liveData instanceof VNumberArray) { - if (liveData instanceof VDoubleArray) { + if (liveData instanceof VDoubleArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - double value = ((VDoubleArray) liveData).getData().getDouble(index); + double value = array.getData().getDouble(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VDouble.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } - } else if (liveData instanceof VFloatArray) { + } else if (liveData instanceof VFloatArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - float value = ((VFloatArray) liveData).getData().getFloat(index); + float value = array.getData().getFloat(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VFloat.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } - } else if (liveData instanceof VIntArray) { + } else if (liveData instanceof VIntArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - int value = ((VIntArray) liveData).getData().getInt(index); + int value = array.getData().getInt(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VInt.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } - } else if (liveData instanceof VLongArray) { + } else if (liveData instanceof VLongArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - long value = ((VLongArray) liveData).getData().getLong(index); + long value = array.getData().getLong(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VLong.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } - } else if (liveData instanceof VShortArray) { + } else if (liveData instanceof VShortArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - short value = ((VShortArray) liveData).getData().getShort(index); + short value = array.getData().getShort(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VShort.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } - } else if (liveData instanceof VByteArray) { + } else if (liveData instanceof VByteArray array) { for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { - byte value = ((VByteArray) liveData).getData().getByte(index); + byte value = array.getData().getByte(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VByte.of(value, Alarm.none(), Time.now(), Display.none())); + columnEntry.setLiveVal(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } } - } else if (liveData instanceof VBooleanArray) { - ListBoolean listBoolean = ((VBooleanArray) liveData).getData(); + } else if (liveData instanceof VBooleanArray array) { + ListBoolean listBoolean = array.getData(); for (int i = 0; i < listBoolean.size(); i++) { boolean value = listBoolean.getBoolean(i); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VBoolean.of(value, Alarm.none(), Time.now())); + columnEntry.setLiveVal(VBoolean.of(value, array.getAlarm(), array.getTime())); addRow(i, columnEntries, columnEntry); } - } else if (liveData instanceof VEnumArray) { - List enumValues = ((VEnumArray) liveData).getData(); + } else if (liveData instanceof VEnumArray array) { + List enumValues = array.getData(); for (int i = 0; i < enumValues.size(); i++) { ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VString.of(enumValues.get(i), Alarm.none(), Time.now())); + columnEntry.setLiveVal(VString.of(enumValues.get(i), array.getAlarm(), array.getTime())); addRow(i, columnEntries, columnEntry); } - } else if (liveData instanceof VStringArray) { - List stringValues = ((VStringArray) liveData).getData(); + } else if (liveData instanceof VStringArray array) { + List stringValues = array.getData(); for (int i = 0; i < stringValues.size(); i++) { ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VString.of(stringValues.get(i), Alarm.none(), Time.now())); + columnEntry.setLiveVal(VString.of(stringValues.get(i), array.getAlarm(), array.getTime())); addRow(i, columnEntries, columnEntry); } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index bc723e2414..e22713ea2e 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -9,7 +9,7 @@
- + From 9a8d40cdbba3966c81e466bde8dde35ec26673a7 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 5 Dec 2025 14:27:42 +0100 Subject: [PATCH 14/23] Javadoc and code cleanup --- .../ui/snapshot/compare/ComparisonData.java | 29 +++++---- .../ui/snapshot/compare/ComparisonDialog.java | 19 ++++++ .../TableComparisonViewController.java | 61 ++++++++----------- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java index 836c0416f6..42b0e7db10 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java @@ -6,26 +6,25 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; -import org.epics.vtype.VBooleanArray; -import org.epics.vtype.VByteArray; -import org.epics.vtype.VDoubleArray; -import org.epics.vtype.VFloatArray; -import org.epics.vtype.VIntArray; -import org.epics.vtype.VLongArray; -import org.epics.vtype.VNumberArray; -import org.epics.vtype.VShortArray; -import org.epics.vtype.VTable; -import org.epics.vtype.VType; - -import java.util.ArrayList; + import java.util.List; +/** + * Data class for the {@link javafx.scene.control.TableView} of the comparison dialog. + */ public class ComparisonData { + /** + * Index (=row number) for this instance. + */ private final IntegerProperty index = new SimpleIntegerProperty(this, "index"); - private List columnEntries; + /** + * {@link List} of {@link ColumnEntry}s, one for each column in the data. For array data this will + * hold only one element. + */ + private final List columnEntries; - public ComparisonData(int index, List columnEntries){ + public ComparisonData(int index, List columnEntries) { this.index.set(index); this.columnEntries = columnEntries; } @@ -35,7 +34,7 @@ public IntegerProperty indexProperty() { return index; } - public List getColumnEntries(){ + public List getColumnEntries() { return columnEntries; } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java index 3af5d045bc..442dd2a170 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -15,8 +15,27 @@ import java.io.IOException; import java.util.ResourceBundle; +/** + * Dialog showing a {@link javafx.scene.control.TableView} where array or table data is visualized element wise. + * Purpose is to be able to inspect deltas on array/table element level. + * + *

+ * Data in the {@link javafx.scene.control.TableView} is organized column wise. Each row in the {@link javafx.scene.control.TableView} + * corresponds to an individual element in the data. Each column contains three nested columns: stored value, + * delta and live value. + *

+ *

+ * For an array type ({@link org.epics.vtype.VNumberArray} the table will thus hold a single data column. For a + * table type ({@link org.epics.vtype.VTable} there will be one data column for each column in the table. + *

+ */ public class ComparisonDialog extends Dialog { + /** + * Constructor + * @param data The data as stored in a {@link org.phoebus.applications.saveandrestore.model.Snapshot} + * @param pvName The name of the for which + */ public ComparisonDialog(VType data, String pvName){ getDialogPane().getButtonTypes().addAll(ButtonType.CLOSE); diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 2ca880c62b..2aa502fe7a 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -12,9 +12,6 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import org.epics.util.array.ListBoolean; -import org.epics.vtype.Alarm; -import org.epics.vtype.Display; -import org.epics.vtype.Time; import org.epics.vtype.VBoolean; import org.epics.vtype.VBooleanArray; import org.epics.vtype.VByte; @@ -50,6 +47,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * Controller class for the comparison table view. + */ public class TableComparisonViewController { @SuppressWarnings("unused") @@ -101,6 +101,11 @@ public void initialize() { deltaColumn.setCellFactory(e -> new VDeltaCellEditor<>()); } + /** + * Loads snapshot data and then connects to the corresponding PV. + * @param data Data as stored in a {@link org.phoebus.applications.saveandrestore.model.Snapshot} + * @param pvName The name of the PV. + */ public void loadDataAndConnect(VType data, String pvName) { pvNameProperty.set(pvName); @@ -112,39 +117,27 @@ public void loadDataAndConnect(VType data, String pvName) { if (data instanceof VDoubleArray array) { double value = array.getData().getDouble(index); ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VFloatArray array) { float value = array.getData().getFloat(index); ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VIntArray array) { int value = array.getData().getInt(index); ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VLongArray array) { long value = array.getData().getLong(index); ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VShortArray array) { short value = array.getData().getShort(index); ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VByteArray array) { byte value = array.getData().getByte(index); ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } } } else if (data instanceof VBooleanArray array) { @@ -153,32 +146,24 @@ public void loadDataAndConnect(VType data, String pvName) { List columnEntries = new ArrayList<>(); boolean value = listBoolean.getBoolean(index); ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, array.getAlarm(), array.getTime())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } } else if (data instanceof VEnumArray array) { List enumValues = array.getData(); for (int index = 0; index < enumValues.size(); index++) { List columnEntries = new ArrayList<>(); ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(index), array.getAlarm(), array.getTime())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } } else if (data instanceof VStringArray array) { List stringValues = array.getData(); for (int index = 0; index < stringValues.size(); index++) { List columnEntries = new ArrayList<>(); ColumnEntry columnEntry = new ColumnEntry(VString.of(stringValues.get(index), array.getAlarm(), array.getTime())); - columnEntries.add(columnEntry); - ComparisonData comparisonData = new ComparisonData(index, columnEntries); - comparisonTable.getItems().add(index, comparisonData); + addRow(index, columnEntries, columnEntry); } } - connect(); - } private void addRow(int index, List columnEntries, ColumnEntry columnEntry) { @@ -187,7 +172,10 @@ private void addRow(int index, List columnEntries, ColumnEntry colu comparisonTable.getItems().add(index, comparisonData); } - public void connect() { + /** + * Attempts to connect to the PV. + */ + private void connect() { try { PV pv = PVPool.getPV(pvNameProperty.get()); pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) @@ -197,6 +185,11 @@ public void connect() { } } + /** + * Updates the {@link TableView} from the live data acquired through a PV monitor event. + * Differences in data sizes between stored and live data is considered. + * @param liveData EPICS data from the connected PV, or {@link VDisconnectedData#INSTANCE}. + */ private void updateTable(VType liveData) { if (liveData.equals(VDisconnectedData.INSTANCE)) { comparisonTable.getItems().forEach(i -> i.getColumnEntries().get(0).setLiveVal(VDisconnectedData.INSTANCE)); @@ -218,7 +211,7 @@ private void updateTable(VType liveData) { } else if (liveData instanceof VFloatArray array) { columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VShortArray array) { - columnEntry.setLiveVal(VShort.of(array.getData().getShort(index),array.getAlarm(), array.getTime(), array.getDisplay())); + columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); } } else if (liveData instanceof VBooleanArray array) { liveDataArraySize.set(array.getData().size()); From 21e8517496ee2a39cb4d49f243a62c3970df2ee5 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 8 Dec 2025 09:11:49 +0100 Subject: [PATCH 15/23] Support for setting threshold when comparing array/table elements --- .../ui/snapshot/VDeltaCellEditor.java | 5 +- .../ui/snapshot/compare/ColumnEntry.java | 20 +++++++- .../ui/snapshot/compare/ComparisonData.java | 15 ++++++ .../TableComparisonViewController.java | 48 ++++++++++++++++++- .../snapshot/compare/TableComparisonView.fxml | 20 +++++--- .../compare/ComparisonDialogDemo.java | 4 +- 6 files changed, 99 insertions(+), 13 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java index 07e5840430..e072c002d0 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java @@ -78,7 +78,10 @@ public void updateItem(T item, boolean empty) { setStyle(TableCellColors.DISCONNECTED_STYLE); } else if (pair.value == VNoData.INSTANCE) { setText(pair.value.toString()); - } else { + } else if(pair.base == VNoData.INSTANCE){ + setText(VNoData.INSTANCE.toString()); + } + else { Utilities.VTypeComparison vtc = Utilities.deltaValueToString(pair.value, pair.base, pair.threshold); if (vtc.getValuesEqual() != 0 && (pair.base instanceof VNumberArray || diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java index 013356a54e..6246262ab3 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ColumnEntry.java @@ -6,9 +6,12 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import org.epics.vtype.VNumber; import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.SafeMultiply; import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.saveandrestore.util.Threshold; +import org.phoebus.saveandrestore.util.Utilities; import org.phoebus.saveandrestore.util.VNoData; import java.util.Optional; @@ -31,7 +34,7 @@ public class ColumnEntry { */ private final ObjectProperty liveValue = new SimpleObjectProperty<>(this, "liveValue", VNoData.INSTANCE); - private final Optional> threshold = Optional.empty(); + private Optional> threshold = Optional.empty(); public ColumnEntry(VType storedValue) { this.storedValue.set(storedValue); @@ -54,4 +57,19 @@ public ObjectProperty liveValueProperty() { public ObjectProperty getDelta() { return delta; } + + /** + * Set the threshold value for this entry. All value comparisons related to this entry are calculated using the + * threshold (if it exists). + * + * @param ratio the threshold + */ + public void setThreshold(double ratio) { + if (storedValue.get() instanceof VNumber) { + VNumber vNumber = SafeMultiply.multiply((VNumber) storedValue.get(), ratio); + boolean isNegative = vNumber.getValue().doubleValue() < 0; + Threshold t = new Threshold<>(isNegative ? SafeMultiply.multiply(vNumber.getValue(), -1.0) : vNumber.getValue()); + this.delta.set(new VTypePair(liveValue.get(), storedValue.get(), Optional.of(t))); + } + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java index 42b0e7db10..7026add0d2 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonData.java @@ -6,8 +6,13 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; +import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.ui.VTypePair; +import org.phoebus.saveandrestore.util.Threshold; +import org.phoebus.saveandrestore.util.Utilities; import java.util.List; +import java.util.Optional; /** * Data class for the {@link javafx.scene.control.TableView} of the comparison dialog. @@ -37,4 +42,14 @@ public IntegerProperty indexProperty() { public List getColumnEntries() { return columnEntries; } + + /** + * Set the threshold value for this entry. All value comparisons related to this entry are calculated using the + * threshold (if it exists). + * + * @param ratio the threshold + */ + public void setThreshold(double ratio) { + columnEntries.forEach(columnEntry -> columnEntry.setThreshold(ratio)); + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 2aa502fe7a..c61bdcb60e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -8,9 +8,14 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; +import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.util.converter.DoubleStringConverter; import org.epics.util.array.ListBoolean; import org.epics.vtype.VBoolean; import org.epics.vtype.VBooleanArray; @@ -31,6 +36,7 @@ import org.epics.vtype.VString; import org.epics.vtype.VStringArray; import org.epics.vtype.VType; +import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.applications.saveandrestore.ui.snapshot.VDeltaCellEditor; import org.phoebus.applications.saveandrestore.ui.snapshot.VTypeCellEditor; @@ -68,11 +74,14 @@ public class TableComparisonViewController { @FXML private TableColumn liveValueColumn; - @SuppressWarnings("unused") @FXML private TableColumn deltaColumn; + @SuppressWarnings("unused") + @FXML + private Spinner thresholdSpinner; + @SuppressWarnings("unused") @FXML private Label pvName; @@ -99,11 +108,18 @@ public void initialize() { deltaColumn.setComparator(Comparator.comparingDouble(VTypePair::getAbsoluteDelta)); deltaColumn.setCellFactory(e -> new VDeltaCellEditor<>()); + + SpinnerValueFactory thresholdSpinnerValueFactory = new SpinnerValueFactory.DoubleSpinnerValueFactory(0.0, 999.0, 0.0, 0.01); + thresholdSpinnerValueFactory.setConverter(new DoubleStringConverter()); + thresholdSpinner.setValueFactory(thresholdSpinnerValueFactory); + thresholdSpinner.getEditor().setAlignment(Pos.CENTER_RIGHT); + thresholdSpinner.getEditor().textProperty().addListener((a, o, n) -> parseAndUpdateThreshold(n)); } /** * Loads snapshot data and then connects to the corresponding PV. - * @param data Data as stored in a {@link org.phoebus.applications.saveandrestore.model.Snapshot} + * + * @param data Data as stored in a {@link org.phoebus.applications.saveandrestore.model.Snapshot} * @param pvName The name of the PV. */ public void loadDataAndConnect(VType data, String pvName) { @@ -188,6 +204,7 @@ private void connect() { /** * Updates the {@link TableView} from the live data acquired through a PV monitor event. * Differences in data sizes between stored and live data is considered. + * * @param liveData EPICS data from the connected PV, or {@link VDisconnectedData#INSTANCE}. */ private void updateTable(VType liveData) { @@ -310,4 +327,31 @@ private void updateTable(VType liveData) { } } } + + private void parseAndUpdateThreshold(String value) { + thresholdSpinner.getEditor().getStyleClass().remove("input-error"); + thresholdSpinner.setTooltip(null); + + double parsedNumber; + try { + parsedNumber = Double.parseDouble(value.trim()); + updateThreshold(parsedNumber); + } catch (Exception e) { + thresholdSpinner.getEditor().getStyleClass().add("input-error"); + thresholdSpinner.setTooltip(new Tooltip(Messages.toolTipMultiplierSpinner)); + } + } + + /** + * Computes thresholds on scalar data types. The threshold is used to indicate that a delta value within threshold + * should not decorate the delta column, i.e. consider saved and live values equal. + * + * @param threshold Threshold in percent + */ + private void updateThreshold(double threshold) { + double ratio = threshold / 100; + comparisonTable.getItems().forEach(comparisonData -> { + comparisonData.setThreshold(ratio); + }); + } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index e22713ea2e..19099a9ca9 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -5,7 +5,19 @@ + + + + + +
@@ -24,11 +36,5 @@
- - - +
diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 94d25dd2bd..4562d0fae8 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -26,11 +26,11 @@ public static void main(String[] args) { public void start(Stage primaryStage) { VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(1, 2, 3, 4, 5), + VDoubleArray.of(ArrayDouble.of(1, 2, 3), Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(1, 8, 7, 0)"); + new ComparisonDialog(vDoubleArray, "loc://x(1, 1.9, 4, 8)"); comparisonDialog.show(); } } From 357bdd865e25a34b9d25d2dfc2378bb982e6ab57 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 8 Dec 2025 09:19:04 +0100 Subject: [PATCH 16/23] Cleanup code when comparison dialog is closed --- .../ui/snapshot/compare/ComparisonDialog.java | 1 + .../compare/TableComparisonViewController.java | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java index 442dd2a170..9f531b8e4f 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialog.java @@ -51,6 +51,7 @@ public ComparisonDialog(VType data, String pvName){ TableComparisonViewController controller = loader.getController(); controller.loadDataAndConnect(data, pvName); getDialogPane().setContent(node); + setOnCloseRequest(e -> controller.cleanUp()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index c61bdcb60e..2cb6cbce76 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -88,6 +88,8 @@ public class TableComparisonViewController { private final StringProperty pvNameProperty = new SimpleStringProperty(); + private PV pv; + /** * The time between updates of dynamic data in the table, in ms. */ @@ -193,7 +195,7 @@ private void addRow(int index, List columnEntries, ColumnEntry colu */ private void connect() { try { - PV pv = PVPool.getPV(pvNameProperty.get()); + pv = PVPool.getPV(pvNameProperty.get()); pv.onValueEvent().throttleLatest(TABLE_UPDATE_INTERVAL, TimeUnit.MILLISECONDS) .subscribe(value -> updateTable(PV.isDisconnected(value) ? VDisconnectedData.INSTANCE : value)); } catch (Exception e) { @@ -201,6 +203,15 @@ private void connect() { } } + /** + * Returns PV to pool, e.g. when UI is dismissed. + */ + public void cleanUp(){ + if(pv != null){ + PVPool.releasePV(pv); + } + } + /** * Updates the {@link TableView} from the live data acquired through a PV monitor event. * Differences in data sizes between stored and live data is considered. From fa7bb3c8fe8ee613fa2d1d18efe63b30adb6c653 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 8 Dec 2025 11:32:16 +0100 Subject: [PATCH 17/23] Use VTypeHelper to determine array size --- .../TableComparisonViewController.java | 107 ++++++++++++++---- .../compare/ComparisonDialogDemo.java | 2 +- .../org/phoebus/core/vtypes/VTypeHelper.java | 2 + .../phoebus/core/vtypes/VTypeHelperTest.java | 4 + 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java index 2cb6cbce76..5d4c86f3f0 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonViewController.java @@ -36,11 +36,20 @@ import org.epics.vtype.VString; import org.epics.vtype.VStringArray; import org.epics.vtype.VType; +import org.epics.vtype.VUByte; +import org.epics.vtype.VUByteArray; +import org.epics.vtype.VUInt; +import org.epics.vtype.VUIntArray; +import org.epics.vtype.VULong; +import org.epics.vtype.VULongArray; +import org.epics.vtype.VUShort; +import org.epics.vtype.VUShortArray; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.ui.VTypePair; import org.phoebus.applications.saveandrestore.ui.snapshot.VDeltaCellEditor; import org.phoebus.applications.saveandrestore.ui.snapshot.VTypeCellEditor; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.core.vtypes.VTypeHelper; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.saveandrestore.util.VNoData; @@ -49,7 +58,6 @@ import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -128,8 +136,8 @@ public void loadDataAndConnect(VType data, String pvName) { pvNameProperty.set(pvName); + int arraySize = VTypeHelper.getArraySize(data); if (data instanceof VNumberArray) { - int arraySize = ((VNumberArray) data).getData().size(); for (int index = 0; index < arraySize; index++) { List columnEntries = new ArrayList<>(); if (data instanceof VDoubleArray array) { @@ -144,18 +152,34 @@ public void loadDataAndConnect(VType data, String pvName) { int value = array.getData().getInt(index); ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); + } else if (data instanceof VUIntArray array) { + int value = array.getData().getInt(index); + ColumnEntry columnEntry = new ColumnEntry(VUInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VLongArray array) { long value = array.getData().getLong(index); ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); + } else if (data instanceof VULongArray array) { + long value = array.getData().getLong(index); + ColumnEntry columnEntry = new ColumnEntry(VULong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VShortArray array) { short value = array.getData().getShort(index); ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); + } else if (data instanceof VUShortArray array) { + short value = array.getData().getShort(index); + ColumnEntry columnEntry = new ColumnEntry(VUShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); } else if (data instanceof VByteArray array) { byte value = array.getData().getByte(index); ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); + } else if (data instanceof VUByteArray array) { + byte value = array.getData().getByte(index); + ColumnEntry columnEntry = new ColumnEntry(VUByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); } } } else if (data instanceof VBooleanArray array) { @@ -206,8 +230,8 @@ private void connect() { /** * Returns PV to pool, e.g. when UI is dismissed. */ - public void cleanUp(){ - if(pv != null){ + public void cleanUp() { + if (pv != null) { PVPool.releasePV(pv); } } @@ -222,95 +246,130 @@ private void updateTable(VType liveData) { if (liveData.equals(VDisconnectedData.INSTANCE)) { comparisonTable.getItems().forEach(i -> i.getColumnEntries().get(0).setLiveVal(VDisconnectedData.INSTANCE)); } else { - AtomicInteger liveDataArraySize = new AtomicInteger(0); + int liveDataArraySize = VTypeHelper.getArraySize(liveData); comparisonTable.getItems().forEach(i -> { int index = i.indexProperty().get(); ColumnEntry columnEntry = i.getColumnEntries().get(0); if (liveData instanceof VNumberArray) { - liveDataArraySize.set(((VNumberArray) liveData).getData().size()); - if (index >= liveDataArraySize.get()) { // Live data has fewer elements than stored data + if (index >= liveDataArraySize) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else if (liveData instanceof VDoubleArray array) { columnEntry.setLiveVal(VDouble.of(array.getData().getDouble(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VShortArray array) { + columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VIntArray array) { columnEntry.setLiveVal(VInt.of(array.getData().getInt(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VUIntArray array) { + columnEntry.setLiveVal(VUInt.of(array.getData().getInt(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VLongArray array) { columnEntry.setLiveVal(VLong.of(array.getData().getLong(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VULongArray array) { + columnEntry.setLiveVal(VULong.of(array.getData().getLong(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VFloatArray array) { columnEntry.setLiveVal(VFloat.of(array.getData().getFloat(index), array.getAlarm(), array.getTime(), array.getDisplay())); } else if (liveData instanceof VShortArray array) { + columnEntry.setLiveVal(VUShort.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VUShortArray array) { columnEntry.setLiveVal(VShort.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VByteArray array) { + columnEntry.setLiveVal(VByte.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); + } else if (liveData instanceof VUByteArray array) { + columnEntry.setLiveVal(VUByte.of(array.getData().getShort(index), array.getAlarm(), array.getTime(), array.getDisplay())); } } else if (liveData instanceof VBooleanArray array) { - liveDataArraySize.set(array.getData().size()); if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { columnEntry.setLiveVal(VBoolean.of(array.getData().getBoolean(index), array.getAlarm(), array.getTime())); } - } else if (liveData instanceof VEnumArray array) { - liveDataArraySize.set(array.getData().size()); if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); + columnEntry.setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); } } else if (liveData instanceof VStringArray array) { - liveDataArraySize.set(array.getData().size()); if (index >= array.getData().size()) { // Live data has fewer elements than stored data columnEntry.setLiveVal(VNoData.INSTANCE); } else { - i.getColumnEntries().get(index).setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); + columnEntry.setLiveVal(VString.of(array.getData().get(index), array.getAlarm(), array.getTime())); } } }); // Live data may have more elements than stored data - if (liveDataArraySize.get() > comparisonTable.getItems().size()) { + if (liveDataArraySize > comparisonTable.getItems().size()) { List columnEntries = new ArrayList<>(); if (liveData instanceof VNumberArray) { if (liveData instanceof VDoubleArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { double value = array.getData().getDouble(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } } else if (liveData instanceof VFloatArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { float value = array.getData().getFloat(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } } else if (liveData instanceof VIntArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { int value = array.getData().getInt(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } + } else if (liveData instanceof VUIntArray array) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + int value = array.getData().getInt(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VUInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); + } } else if (liveData instanceof VLongArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { long value = array.getData().getLong(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } + } else if (liveData instanceof VULongArray array) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + long value = array.getData().getLong(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VULong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); + } } else if (liveData instanceof VShortArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { short value = array.getData().getShort(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } + } else if (liveData instanceof VUShortArray array) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + short value = array.getData().getShort(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VUShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); + } } else if (liveData instanceof VByteArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize.get(); index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { byte value = array.getData().getByte(index); ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); addRow(index, columnEntries, columnEntry); } + } else if (liveData instanceof VUByteArray array) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + byte value = array.getData().getByte(index); + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + columnEntry.setLiveVal(VUByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); + addRow(index, columnEntries, columnEntry); + } } } else if (liveData instanceof VBooleanArray array) { ListBoolean listBoolean = array.getData(); @@ -342,10 +401,8 @@ private void updateTable(VType liveData) { private void parseAndUpdateThreshold(String value) { thresholdSpinner.getEditor().getStyleClass().remove("input-error"); thresholdSpinner.setTooltip(null); - - double parsedNumber; try { - parsedNumber = Double.parseDouble(value.trim()); + double parsedNumber = Double.parseDouble(value.trim()); updateThreshold(parsedNumber); } catch (Exception e) { thresholdSpinner.getEditor().getStyleClass().add("input-error"); @@ -354,8 +411,8 @@ private void parseAndUpdateThreshold(String value) { } /** - * Computes thresholds on scalar data types. The threshold is used to indicate that a delta value within threshold - * should not decorate the delta column, i.e. consider saved and live values equal. + * Computes thresholds on the individual elements. The threshold is used to indicate that a delta value within threshold + * should not decorate the delta column. * * @param threshold Threshold in percent */ diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 4562d0fae8..77e31a632a 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -30,7 +30,7 @@ public void start(Stage primaryStage) { Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(1, 1.9, 4, 8)"); + new ComparisonDialog(vDoubleArray, "loc://x(3, 2, 1)"); comparisonDialog.show(); } } diff --git a/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java b/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java index 843db11ae8..6011f7bc21 100644 --- a/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java +++ b/core/vtype/src/main/java/org/phoebus/core/vtypes/VTypeHelper.java @@ -278,6 +278,8 @@ public static int getArraySize(final VType value) { sizes = ((VEnumArray) value).getSizes(); } else if (value instanceof VStringArray) { sizes = ((VStringArray) value).getSizes(); + } else if (value instanceof VBooleanArray) { + sizes = ((VBooleanArray) value).getSizes(); } else { return 0; } diff --git a/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java b/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java index 6c7f6d1393..360034e50e 100644 --- a/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java +++ b/core/vtype/src/test/java/org/phoebus/core/vtypes/VTypeHelperTest.java @@ -192,6 +192,10 @@ public void testGetArraySize() { VStringArray stringArray = VStringArray.of(Arrays.asList("a", "b"), alarm, time); assertEquals(2, VTypeHelper.getArraySize(stringArray)); + + VBooleanArray booleanArray = + VBooleanArray.of(ArrayBoolean.of(true, false, true), alarm, time); + assertEquals(3, VTypeHelper.getArraySize(booleanArray)); } @Test From ba95624e375a3f855963c28d2bdfc9f4f1463359 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 8 Dec 2025 13:02:03 +0100 Subject: [PATCH 18/23] Adding documentation --- .../app/doc/images/compare-arrays.png | Bin 0 -> 43982 bytes .../app/doc/images/snapshot-view-with-delta.png | Bin 0 -> 44638 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/save-and-restore/app/doc/images/compare-arrays.png create mode 100644 app/save-and-restore/app/doc/images/snapshot-view-with-delta.png diff --git a/app/save-and-restore/app/doc/images/compare-arrays.png b/app/save-and-restore/app/doc/images/compare-arrays.png new file mode 100644 index 0000000000000000000000000000000000000000..edb51b85fc18bd763a42b1531f0cc17b305e56ff GIT binary patch literal 43982 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

ykr+v>S)08QC}DM9~Dj1f-Bh%0+>5 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 yR_5V5bpAO>xKBr2MjS Date: Tue, 9 Dec 2025 14:10:29 +0100 Subject: [PATCH 19/23] Return infinite delta if arrays are of different length --- .../applications/saveandrestore/ui/VTypePair.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java index d653876b52..0942d11ef5 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VTypePair.java @@ -20,7 +20,9 @@ import org.epics.vtype.VNumber; import org.epics.vtype.VString; import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VTypeHelper; import org.phoebus.saveandrestore.util.Threshold; +import org.phoebus.saveandrestore.util.VNoData; import java.util.Optional; @@ -59,11 +61,19 @@ public VTypePair(VType base, VType value, Optional> threshold) { * Main use case for this is ordering on delta. Absolute delta may be more useful as otherwise zero * deltas would be found between positive and negative deltas. *

+ *

+ * If {@link #base} or {@link #value} are null or {@link VNoData#INSTANCE}, then + * the delta cannot be computed as a number. In this case {@link Double#MAX_VALUE} is returned + * to indicate an "infinite delta". + *

* @return Absolute delta between {@link #base} and {@link #value}. */ public double getAbsoluteDelta(){ - if(base == null || value == null){ - return 0.0; + if(base.equals(VNoData.INSTANCE) || + value.equals(VNoData.INSTANCE) || + base == null || + value == null){ + return Double.MAX_VALUE; } if(base instanceof VNumber){ return Math.abs(((VNumber)base).getValue().doubleValue() - From fc547ad5f587199d4e1a6884f4227a559fb55121 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 9 Dec 2025 14:46:52 +0100 Subject: [PATCH 20/23] Updated ordering and documentation --- .../images/compare-arrays-infinite-delta.png | Bin 0 -> 17946 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/save-and-restore/app/doc/images/compare-arrays-infinite-delta.png diff --git a/app/save-and-restore/app/doc/images/compare-arrays-infinite-delta.png b/app/save-and-restore/app/doc/images/compare-arrays-infinite-delta.png new file mode 100644 index 0000000000000000000000000000000000000000..cc6ccf97fd3d2cb82b45620f3d1025e5414822d4 GIT binary patch literal 17946 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 literal 0 HcmV?d00001 From d355c647d7e63560b6d92fe82356ed5fe5b88bb3 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 9 Dec 2025 14:52:51 +0100 Subject: [PATCH 21/23] Updated ordering and documentation --- app/save-and-restore/app/doc/index.rst | 23 +++++++++++++++++++ .../compare/ComparisonDialogDemo.java | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index f826a173fc..bdb3de6d1f 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -303,6 +303,29 @@ are shown by default. The left-most columns in the toolbar can be used to show/h .. image:: images/toggle-readback.png :width: 80% +While comparison of scalar values in the snapshot view is straight-forward, array (or table) type data is difficult +to compare from the single table cells. User may instead click on the highlighted ":math:`{\Delta}` Live" cell to launch a dialog +showing stored, live and :math:`{\Delta}` for the selected PV: + +.. image:: images/snapshot-view-with-delta.png + +User clicks "Click to compare": + +.. image:: images/compare-arrays.png + +The threshold settings works in the same manner is in the snapshot view and operates on each element (row) in the +table view. + +In case the stored and live value of the array/table data are of different dimensions, cells where no value is available +will be rendered as "---". Moreover, since in these cases an absolute delta cannot be computed, the delta column will also show +"---". + +User may click the table header of the delta column to sort on the delta value to quickly find rows where either the +stored or live value is not defined (due to difference in dimension). For such rows the absolute delta will be treated +as infinite, which impacts ordering on the delta column: + +.. image:: images/compare-arrays-infinite-delta.png + Restoring A Snapshot -------------------- diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index 77e31a632a..cb945566a0 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -30,7 +30,7 @@ public void start(Stage primaryStage) { Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(3, 2, 1)"); + new ComparisonDialog(vDoubleArray, "loc://x(3, 2, 1, 1)"); comparisonDialog.show(); } } From 24f8a08bb1a1d614296719f314105f66a604717a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 10 Dec 2025 15:40:56 +0100 Subject: [PATCH 22/23] Add dimensions and non-equal count to comparison dialog --- .../app/doc/images/compare-arrays.png | Bin 43982 -> 50178 bytes .../TableComparisonViewController.java | 203 ++++++++---------- .../saveandrestore/messages.properties | 3 + .../snapshot/compare/TableComparisonView.fxml | 81 +++++-- .../compare/ComparisonDialogDemo.java | 4 +- 5 files changed, 159 insertions(+), 132 deletions(-) diff --git a/app/save-and-restore/app/doc/images/compare-arrays.png b/app/save-and-restore/app/doc/images/compare-arrays.png index edb51b85fc18bd763a42b1531f0cc17b305e56ff..f5a80fb9ec78b3bf562d4a110066050fd3204210 100644 GIT binary patch literal 50178 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 literal 43982 zcmZU)1yo!~^FE9Q7y<+jfuI2fx8UyX9tf_%oxvfv1a~KBaGybg2TvfuU4y&Z|7Lf8 zyYKlv&Y3fpzFpm2)l${ZQxl=0B#i-j1%iWv!;qDcP=kX*P=SMkpGJKKl(=-7LE+#) zb=KnIDzf6@N_|APu`Q_V(1bga=#V5(8;OKzkA~*$04Co z2aAZ*Qq(~aY(yoIl9166Kvi<_jV4`9V6gl97uYIOeL>*THguBi!;Z5heuuGi{^fN3 zr~9*UnEx0O9D5A)i^8uHaD}HFAzeAl4c|(wUfCnX!I8cQq#&=EAwrNc%3~2RTY7vi zw21OOy4~h&;@7BgYJ!NFeHf6Z=dVYOa`T0@Di%gaE!z*PQhn$Q(Xc#=I12 zMLjlCqC?HyE9CoQP@n3)BarbA~7M^gc>3t7i$*vb{CQsrOucT$@V} z_aQLR@AJzC>-Z2bFHf2q8)7+;sc}3yyOXN7adEKylwUxZ5_`}WGNrJTDui-jPLKLh z&?u0wd~?MC&M(7B5HWG-Rhe&9I z5d^2EN#mipHDatkiBula7Tl}w3K8a{|0;2u%Rr2So3k0gXzOvzmz0cjY)L*m((t%w z5&mm2X5bt$83RE8SE2lA2D;FJIGjv7(h6fH6OP5`&36v0=cnc5$QuEaD##fkST89! z-ywRhJYNXpMup2K2UWn?#-XYOzA*dPXha2ulQiO=!59+7=YjhaV7h{2g>3m%(h+|F z$wP$26XEtNxhG=(S6WtN(HEGM;&?LHY9AXY#J`agh<}RX-#|weS5;w5f;*wWZXlRL z!WX4h;nl$SCE6)ck{|Wmt(4dc=|Y_9ySEjNRM#6FOszmSBiuAh4kI!iJOfPHuRD&I zSy&Ds!z-_@!3zk6!4BQ+j%4@GNTuPEkRl`1q>1u>gS9o_=wIQ-;uK7M2B{X6zS5xj z^7>aqOVqA-OhHFptraLEluK;>bNhE=D=xm*?WFIcnWf1@oAX#c9~IpFF#Ub>l3j|( zG`zNll`TFENr9^vPCb%0+PYuA>)hlSTW*aGBYt^QW0#ll=Nj1=&KZ+^AzR!_geGGC zfY$@7j*&d#S%}PV1Ks9pAV+T}X{T)`L8$10WgC(KhQp_;uhQ!`*8)Ct9i;x4%OAU6lbJ(WUOcxXrAK~hqFt(FF<5VN|RWSVWi^1seX}*BP=5$V#lip2jx9Ml;ThVvYbD9~{?FKDk zw+-irJ5EHA(dh~S3SSgz71~GK(v5YqDn3=TGD>q9UIy$_&%%Qu3uK*&YUnb-D0JH3 zkIp*0?|F;$6ZGR3*kH^sHi#}n{>Je}^A!Koa`Ubf`~@>vRx#iHb57n;i4wD#Zm&-7 z`v!B>^@1EpTu>B#7Csad0#XFEgAya(N_zB}^}UX)j2ut;F854MRBl%8F1ZRE!X3-bEa+szA?`l@&4Z{s=Zsu+*Zt-4?Ua7Z9p;c08QaMtI{Tzw~H19bKTv9E< zw$f5EQgWx>RX(d6m_xE_aWA+eJy1G8d;LBeHG7n=lCP%))S|FRviNMV#LG;Ow%xV; zRlE1Cj&F-!-J{S0|FfFFWYGfAkS;s%neTIPmeBDUZ#0XD1DwZ%3~H=s8L8)=89zV! z>}UF=w%RGjsr*C_mnqRI@ik30&9Y*pg1Q1$h7emK=NZ>9hYh#4gT2uhS2b&=rD)S} z&5Y3xi<7a5?+G@r<PRwx#YBDRnS#u+GdT13^l z)h-%&_g_Y3H@+`kL~9j3h}lm(*gFVI-DTcn7KFNf73~S!R5|#*E#ngHc0XG?F1r}9 zsj+_Etvi{%?lS`k&mWgDAu(@rV{^l9dE2T0or6`mw6BCZpSd!)?m4HrVY#q)_#fu4 z#;^QXVcW9ag!P{V5%v!c*n~|Z7K0K;D@;E@<~^&=I`+j+(N5^DAgeYj4Ci)NA0OHt z+;0UhrZ2benx6&0S$rJ}uncSo+zIS_X8aN1BYltoawc*$#^Lk);ELyEST}gPI4w-) zvS|is;0h{zdI6d?W*^FKrh+#GEE8{>-*1e9i@*jNdT*Ke1a2MAui3B6TT?Omb^7pn z7YwUfeSV$pF(hdfR`9f{eW1449Mn;;m9X{Q*Vck759K5sh%S#NHf^oDs#$i-;%()j z(nqmJ^8hbVbiDE7>7p>U?3O7|H?dv=k0)!7d8ck9Yb3voCyCR~6v)PjpGb;Sj!_gG zP2HqDV<{;su9Vv&cnJa_`R=&yv`!8L3w8Gpsq~*lH!1L%skBf};69M=jysQh`F5N2 zI>*;Jd4&G)i(WZ~mV>Tf(t5m$6^0jvx8lINi$>z<%WBh#as#Z&SiR+1Q5W&Y&@TP) z1uEhQ;zaJp6w3_lgexcY-r`H2S!T+W$d*pXEyVI_o!&tnjR-+?|Fw zM-i+B`Gf8v`&7ov+%&Y)vQBa+ zs~6kqn#jP=C)!j>mc>>QSJ+qWi$5lK0ibp^KoBAmLHVV!Kx?>)w;(j1E3x|Q<>k)rR zKTWrg(~Yq`^JJ~BMv zpMDMZGi;nbnra8F$L8@+-n83(tiZrr>_^eoUu8xu$R@GjV&t z5Mo7MkUs|3RP$o*o>73mc^vg)aP-1D9ICe$J`Ze#jGcS(sp!!E9c$t*ocqZ~^u=7X z&)*x)e1ITU%Unm+LQxTp9(YEDgAcQYLjs=QfzK=80|$qg5DJF^eB%Hgi9Ce=dyAlw zhxmWb@Y8=PimHpt$^zf&rq1T(_AXWquJb)YLO@ef)*3plI*JN>rVe(@#%2yC=FFaU zj(}&@V{%VBTYDEiPeIDRYVZNie~MWs$^WY2Y9mOgqo_hI?%-@r&c)2i z%t|Q)A}1#oa5l5xQ6K|N{f*I5BeNY66E$0?Fl`zbHmyJBJ`gYvNTqnY@Q~mA{+k z!>*g+QcI)FnWQ8d7@j&0DJcL)1woFSKCrD#fKKyEQc`4w8t(tR5kY&2aC380XF7nX znRo63)D`(hP(%PprC_KxQFM|%FhR!X&V7kX;?!e9zH9=b)}oQKQFhSuCI>WU`KJ?+ z@Bn%h3cQxjNFbbdmt#exK{@G~t zgHpG{2X=q6olE18Os`p}l+8DhCyLlb!9Y4&XIkFI<&gH=@H+6m#jAL5uK0FoqH=Z6 z*)OqE2Bvn>$dc2-X*Wn7aN1yP+EL4w%(ShWJ>+?|xCCjjyTd$ru9_qHCE9kOMw4DE z@_qiwtC_>}x>uaGFRyrW?RWR4t5b!1Z)NH&MmIk3AIf;RB)oeH$EGs7*!|_a1lg9I zf~lUij`)xa#>CGH5(VO?LCl{uK+h zW3yj=4S8YxSWGEqHz6Ni*QK|hZ=pX_(nsoxSH(=Rg_irTh6XN3FQ+%IKI`;cWn(f{qLfmc;$NSCm-TEcgHs-`E={FgBYR);)1@8}pKIy;A)OSA z95$7!{}T5={1OLj6gao9IByL3vP&noMkgH7i(dY&yx7*TMS&~!QG`Cr&m#YgJ)Anh zwZhLYn!^lLMv%)isQGZppvlo_Q$BYi_M7T)?|Ge^55MMqbp%Oz>7EsNjb^Yr^qD|> zm-wBXCS5*8PV!Kzgu~?)GrzCv&-^e?f3d5{faietVY-YyjBHkoUh~v|of6f&{qc5> z<}#9@D$X>Pw|}3b@9ei}?BHDvg4^jJZKjQB_*LBFSiL*rMODbwXr{dOIG@{$aaj8K z!|g?4$Wo)kjI?S0H!}Zw;~~D|uZZT~hgpb+wOSvpQf;c*wdge~z+4Y>2D7F@k2+Ic zEP{6J*YeCAI+ru1SmPg>-iKn5F&p|_B*QM#ceyu)6@Ncumb<%Rp(PWSApwD8k_j#72}{~wp)s|Fb3JCu8GjYx%&9KW~on? zL)))%JB_%+z1@3D$a*!<}k7;V^d;Se?}8h)-3nhl*FEh(B_~Dw!yemgk;pG z>yhkENyik>F8$9{%M7b?AhPMliyo&PB{k$+55op9;239j^Vy`6R~EWRig>z@Vi|dh zk22u8L+M|oNBeQvv!}k)*I~`>aT*0H1(dT%w1=xjG#7A$xyJO>=F>7{`!%lik9zQA zC`Ha%P+r>ofI7&iMtibY?4eh^U(3O3m_=Y=!1tl?q}GHUB)LN|J}24URE13?{VJro zw#274{5{mY>S8VgfyaA{gm?EF)#&j2^ss$B?$|uXnK*NceAbaCsm|) ztsLYp`6btRj+7SyV~T=LEN@CHX#%gv9{N7?p|4fvOq`{aDC7&=BHL{EUM;gI?B#E8 zUG0kO6k_@1J75Xl*WCZ63)gIOuQX^J%0jVk$!o$d5w?HbQjL`KRS5x^QAN*kG>LsQ zL(06zl@ypcLIIcisX07mK_`W6CR7)flakz~U2SWX`iSkWy;!GDg4AEyraN*86g&{z zw9$H{jOM2fa=PP>e9g6sXFe^Vdt^_71*=x7gUpdv@1^raYxaLu8yY&Y`&oY{{~QeU zV**xLk?a19YR+*ILlTBh>EjkJpa1o0n1$STe~fIG%h`#~(paBe^*tUg*v*IVOc!~- z)$0qc3VvtsPD!g=Y7itSa1h*^R#0|w+2en5@aIUuowLBFy?oExtL|>!exZfyf8aJv zw1-7d<hn3Q*$3IQ=htEwy~3Vt>0!1-D#kkUb2c}ILz z47*3nZ{KMkBxyxwZir{j{oMCA#m#CMnOWUcnE7ZjQRjmN9uK2;Nv?ZFM8@w%3^KY3 zS+J3o-@}Er?jBt$I8LEx!oDI1DyxI`VIFYRq#pkFDPnrD_Q?S_ag3s&OOy2QB2XR4QEzKsqnpciUJv0P;9Kj$57Vv);FK>kS*B%AXM*0@cidJc?sc00Na zD!p;Og68SBHLf@2hs^RX$$l=l-yI+q5cz8d*4?PSIhYPqP;LQfFxoMwM0ltq2|Ti& z>!2k`AQUYqrP|dt+eRIS{w|*1YTvA_5PfU>;nPki`DQqez3U~#3k{3;S9b18cpicq z-NhGcO}4X)j5c`%NNdz8Rr*A$@~xwpO|RrNj2^*q1O%8B`8pUir8=vVXTyTI1@rc; ziXVQK8c!Ix)ygINs;~=cldMLuBFVG*siO|zDIMr!)?i|I-deBgnN#{cu_+QLQof&$ z$@EMyQLdnlq~n-p@lW3q5MqHSU0h=rhj=e#j@1w4XL6~%x96`N@qN76mg3!g`bKpc z+j`_4#i~V(r&oN^aa>Fk|M?T&knO?wd*bn@K^Aa}Ood(c5uNb$SBh&}3N3gpl-9?3 zIj$6e`@C!K)thbeyzTN3!%PM);Z0q0LPIb^ZBxwo$>qRQzTTgc85exaH^8UNsQGo; zeF?vXKe=_APGE1UBH5WX_b@bcaez0Oo8fA&)e#NWmB^J*GlnaVpLEsHboR>X5&zpq zlg-AXaW|VHz4bGY#7-bic!>ZDZbez*|g2rj_Xnzc`WkQbUT15YZ)3GQ@Ivp&mtt`j=V19T;S%~dK zVH#iB;EW_UowCj_W!11JD$=)R{3@%W^AZoUTn4X%*?$RFnMbta#3$C#%QDKJkbuab z^gf9FE7_z2hhX0pL+tp_dr+>hU4!X-vt|x8`fzkPbgnI1FeIV~By>iyk>1}+L}uKA z)x~a(yiAJ}c5Rn~6H{s1;B5hCkR>HH_P> zF>gjd`)@AVkU7Ue^m6M-A`}P$0mp%JmYq`0G6Mu#=W|VVVIF?=SEN?+V)F+ywqb9V z%2T+R8pE1vEP_)pszcS4cqN(7J80HLztL`(4t(O2!}a6$sSVZlz4+x^xSjd26U%Ub zO;Pfmt+iv$rJ6X=c-lX{+b$!qJ>-?HS$wszp66r`_mb!5h+W7btiStXqXbNlfdC+YrMoV<&pemZbe-Kf>7RvnDT4GGzT~Xs!9=%XBHpW#FM!2n*gezZTq8 zp#7u@$|UO}N=Zzt+b}!)_4rC}bPwB9bmY?MGrz8@NJkIQNN#cIfRAiwP$jXA>mHwHd+bq+97B|l-^cQG9qiss9;mOZO&gEJiOn6eOB%tD@b$?#3 zU^O`SQDQcU@|3t~Djd($N}@Xq5yIFUg`W^Jw=I>NU%<&Q~GAw~00)S5S< zqwn}%*DXf#3!8a}M>dh!Wn`fEIVIEj4}&?fP9<7ou#EhPBzgN`=@-e@r?H!aFXn>G zN2`C%oJJ^XWKv$-gb*6AM!t4C{Y5;48x$0#7P!fnb)7t-Gyh82U#Dl7xWe%lAqbtG zJEaM~Q8?sP0lZuu5-!**kiLttVNZ^n7Imd?ED9tX-#cx-gHs_Z?2T^9eAZ##qnlHW z{+TX*(4JwKaK*5%fnvG$zBpa7rJwO6antn(BX8NdQiGw#ZVh|D*0o ziN4AB7rKADfu~ksNBfOuFi7HY(D+Z+K-qJkReZ!83qY@b@D@25c+EVyEvQZPzp{ZQ zF93~S9=|RBPmmoA(2afIy?5omL&W>|N4J0y-Twp=WC7h0cn{}n{?qN{AKjh_jDGo7 zFiGK$ZX-_92LGcQxeATilJn+}WaNb7zrrGD8v=n$dQB4hrV_{>20p#U=CBU_cTjmq za_sQg-3)`Dt9`ZqRVY$|ya1VA*wEh;DkH&}_;2mFGhiS?HxyHt^#58kf+kPhGlJ&J zCf&b9yvia>D!jtIbE1Eb;+r|be3M_}A)8NAE}5R%`=Bv(V+cpW(Ep(^%XLCl7O~TM zlI8}8^pG~s0uV97b(G`8Ua1~{hVW`>V*Xw&e}<(dX9QeprO>|NZw_|Pclfq`Z#F{( zS!QzCAxT!2-YQ8T(-UD#vHv#V5POuks-TAM=Yyh&CQYR?LE#Y+jHBp*L4c%h zYb2d&vRF|*qpS+LJwDfHWBz!5)5p}mp=_qxXZIaA&7;>>0Z$*Lr%`)Wtq+^XLUjCe z5k#j{l>QdeUKmR1`0RbXMGUe2#)D?L*5LWhWDyT$x_Yrf{1b4ArXxzF^w8j-55!Rj z2oq3OgPjjJzC*{0RQkRgia~6)@-=Xas5@mn@0_a&I-WtPn`beWMF(7;AwMTCs{J3z z-&#*dkoumhLVyc#A>?c;U%<-e>1N#jk0a+a#GDC#hVklKvbF)8^lP^yjD)9$Bmd%P zl>mpZ^X)7br5V=|Gyp1l$LGFp^>&inZbG{P68Gb9Fp)O1>#JzBbS$ZJKau5iVg|f7 zfI@73julGa7#C1~1O`qh(<9C{RcG7p`0(jxuA{XbukGFC0Ylhp_cSyydH=If?!LQ# zuOu!%4(i8+oVT+*K=-Ecju*eG4VQg>)I2@j4gZiPV*)IKfp0!}H~vw#-eOQ)S&&-z z@!Aw@7Xrdp(7QX|q4M6Zl?Dr>yk^dlh!Jn7>uwA~PdIx7uJ3WGpQU7rDX+6AR|Sl_k^UfH>C4*; zxc%2tdJf4_F|Yd66u_fb%l-~{r8z(ZH;r{5z3NtEzu58zI7G9)T=I6{mb?0Ey-89A z6yPcjQal1ijfc5xXYkE9`wUX2z6@b6H5rnQ{16yfDuS%}u0-ke>Z(Nu$_YA48@V)z zQ?ROrt!ESg(+I`_Y3n?&ZI%Rjfh(%xS&i*fMfx#y7u0EeBlqdPMBj5Y#QCI;uvK>h z$g3gTy+r4>*Jf_{U7*58Y6fh|B1qnGO~-bQ_d+4|6~uE1P;d)A+@uyL8Jc(KWKtG5 z3BH-g_r1H!Z_^nk^*I*pDi_Y`Ro8;vpPbX;P?fh~zGVG+zP^#DoMQTkw;v1!_k!!S z^_@bVzk2)aoKv5_J7o5j2VNCHC0OWY1D8n12WpI82*+g5rehD=BmnT_hAaRAb^b;X z_R`!7Kvz#^EpyW^1pqK9$b=y3jWbC>GJct0B8?*Z5bjBqN3D-!cdX`;_fh-S{m!rQ z)Kyl8C4rmHr#oTT?w4}2UIkQrAJ_; z%Z6dTzMoy=v!AUif~bdZlT)wTosvxJVgT1eo9=un z3Ps@`K^=HC8pX?E5&wp?N2HK_4Q)HUD@nK2{a}Pga;Vko;=@@H@l~cHOJe-#?6DOH zZ+l$Nq1`|I^*_RspkNN87Qx-@IM(U?2)9*ki#d48l)a zYsYa{n@rYbnmOQ%ruU9r$fsWsv0nDQ z2#E|CP8$=r+pnkk&;m@>>+5o(n5awM4{Z%gooDvTR2Ert^V-QOAgxQ5#Z+Mu#|6G1(P^WX>t`*Ch4nQE+2l=@(=h`d%cb z(*xW>vB2p!DvK}WT9xla6;o?X2SDrA$+VeiF0sv}o*N&rgkDsLr@nXB&A0tkuFVY^ zt1<3*?mhgvKU$FB9lzv=BKy>iz8*Fsl&0p5o~UTCZ80u{XM(nhp^7k=ka(_7J`w+A zLYTE90jyr^9p0ql6-K61bGmeZOPzX&QukD~$m;&xE&%6CnZ|VCjLs0c8TX9r`F0Wf z(6-QiLHhXTGRB5e(+I+wGD%{vK(9Ei3({m*1mSTgLUmY)KTh-u0tt6RwG6^f_w}ln zkaUyFQ15OG?gb;pXrb)6chkjh&l+}I~v^>65e z%y0d?vJzL*oa|?k4db z4aS?BOM`BGykQrwBQ%7tRsf^TWi>;oNc`_liiHvf@5Y<(pCrdjk(P;j`_mDRgmdT? zt{k7pKUpGz-hj}yxs7!^f75axf#ZWIbn4T*w%1b~?XfS3%y4K=ge!J+5^Q~=chG3F zjBeLHF^sbxazw)i8QKD?dj!4FKZU*&_JdKSE$KvCLpB3>2veUC!)3DN&fcK=W?e#Z z54|J{y3=#O!HIonN6L_VH(O9SoYEx@Uj)za43VfThD#7jD!A*aq;ZY|loV61mziry zOrWeQ5U%?!CoFwbIo%#Yf^rcCae102=6+=(*RfNa8BO*Qi&AIB!}U7=`y^)8t>gR8 z+xjunkQOemc3*@vGwPT;b6>Z6EUvIO9_d{-5kq14Kr)WYu+5GcF>5N)f~nn>b~ z4?NQegsiAEGeuQCP~~GZ-<{_+3sy<5{YK~ZE{3YoV;4_COy4jONRd8o8}06TCk457 z@;CAZBFGFMok6HW+*0MV#I8S)e83$u=y@d16)|bW;m_)MNB%0?gN~R1xu{$^5K_uF zk0r>!b#vbHd`DB{)!Be$`fcnp8I#J5xSYc+Cls1Lq z0&zdQoWsS|OrHjqZ*df-eT0VJ#IXd;gt91@$aJso0u3=IPZ_yINFwp-*%bU&2jpK) z8cUNx3Y`#FWJ@{?y4&O`4qE2$@!Z(N>T4wP`kYuHP_o4#_OtlNpQQ$~v~#qgUZ6Ju?#L z*-39&FEp3?n$sq5f8Q+|e%h6Z5X8Pth=({-V|{<}Xqw5KQpW?|bHVn`e}NCS^3W~* z0K`6=U1<|+7_lV0>~D((3H7oTDsqjNr&e$e3xkv^2pJ-SHTCc9*2Kr zEjy}=P^*vqBZ_49!Z}W;smo**4_vl9+v#^-X-SIU{b9UI?61L`t4JY)UD8NTa9RB@ zEimB<)(6?bvS zdwIsqV20eY^fK_=xL~d~4@gjHSSyf{We9hi&O63*wr&c%R_PdP^z17rh2!u_#}rbw zLLGR?ad!&j7i4Zz$d&j$oA5A@o1ktWi^7mgWFWcg9O}b4P#(C5LS`d%T7+L4O#RY; zjbG}BH7*lV_vif1oFbj)*|W9abj|#fNbFLE|JF_+i;HZv;%Maxmm({(J@ZpDPU|O3 z!skv|Nj$5yR5a$n9RV8~XqnVdNn z*w{(MXa+JA#}WJExQQ8ge)NxQT{+m@^bw)x6*CQ!w(2bzL5v*zCLDU-X+YE8$w!Ww z^6Iz%}c*PQH?F$p8+nTGq&mhE)K9+=U(s+cET?SQ`cK5oC8nQ!#B zVcsxwc+iU!20RY~nv=fnSCI)Jz#O|fFN<_FyR8z+Km{#^FUA@zduj-NNPV(<>6QmR zaW?BwIjvwJSN9b74DEn5d6sRy0=EoO&rpu@c&b+cFysBw_HIW1v z0$cB=oa^=`jX-^}Q!JN-U!BDSR~yWUE$&uXdi=A(>k2LgDigWA<~_It7KFH;F%sYB z7oP9_8s_0D5IITt_A|#2O3_aV7S?BNa|^}2Hr8AT9T**rY#l+e@aiB~@80akeQUwx zUuZ|!PoXv&Z2Cj|`okCtPmrl3y2s%=1`>znM^F3iti(2CJ+xPZ6`vQ9AaT9)4a8Az zE=&cxC&Id8Lg;=McSGE(xw8Y~Fqd)X20wnySb*q-q8L^cyfGFm@WJCEMbhFKPs@>3 z$vbumL|!F1LnZ*doQ$TQ6%Sx@tM;#Qsi+HXH4LZuA`P5xh}YD&)zrHyZEIN6ITt9r z+&#MYcHW*d^Ql&Pq;i3VXn6)X=xu3~Z7RX_c*e3thmPHhhy+%6@(aWnUMyt`HZU5P zpGw|0Vtbm8r?z+9$=Ri>LIrC@FFLjEDkLBhtMvm-%(sEqz4(W8mqbUQCy_3lq zgxiph?Uyw1J|WiXX@R*Z#mU`KTuLI`YOvG11%k)J_LSj59e9F?Mc<$D*Fk9g_{S)2 z2(yOAP$G{#LR(D!xeD1-u*}-!yj^DI((UmI13=3bTX%?cNp~yVKyPLT)VlNt<(_ee z=dqn_Z_e21ISq6%4I2`&Al{$Qu}5|?cYXkC;ovF>EJW4;ISjG>a&5g(aO!F znmi|p9Ad&0ar+N7XXv@ArgpuQ&$NA*Yc7p^8@P>^l5W~wIo%x`&hsRGyTjTf4v9p; zndS^a6r5(COgtY4$#EDW%$?RS{=h6IUk&~h!$a!+bkgx{t*uB=Kmk>I&<8=G#EF!h zGepfUXXoM3E~Aj+@LJdyIE%7;(EUrde-jTk`u@xT7sH1A`n+h0J&q z?li)kt##*O$@69fz0h{|1E;zs#?G&dNWR6@JToqaQoLJz7&aUg(31L6X0o#5x<0`; zORt8z)Xqhni^|*tVP@=srhP{RQ@TzK#t5A3&`c%^;VX%HH^*9IHzSd?5W8W=<}96` zVFXe2h85k~AJUY5G*vo^7KER;e>JSYQPyo6M8dEDh@p9q9v)#Qt6PuczW=wo$v)?i zkBKO(SW-S!-DnnWcxLtjel~A*U@ssuXZ;zanc@QLFRDiEUvAT3p#?uqNHM-Sc6sug zl!~WN(Q&axavlbBm(;+8J9dQ zwzcG@BYCQ>`$hQU*aXiCR|x4?dd|>@r`Z|3=40ITc^JCraf@~CL#G_}&TESw^V=c# zi8*n?h72OIL3Lg(QLb`~E(pP*8xq#S*kfNQ?g0%q8dqbj)%3C#g_~Gbi{gQ^hG4nJ z-Jg(C2=y5C+TucSGr9HkDSM!u-?@BfiPYyD7L8YW)zHDzZCPwn_r{z9Wy{P8ua&*Q zQDu*254y`?L|-^>sK$MxpD3&pweMPD>fM~8C+TTf)WztVtaG8biyFYeS z_H5PbaWhZVc^aJ(j7LQHC+LN?l2r-Yx3xOV_YlaFIGBu7ulJ7XA-Oy)j5tPbn00TE z(sk;kD6W4(GmSuomkdCE?f-Iku(;%T>o`Ub9Y3?kk_oiSsb_s_Sj_dB3-7AlO^@)R zpAWMTgVtWxCr-p)6_3{vRu=5dF98vY*ueE!VqM&k(<41t+k2B$JiARZnn`TVVWCgP zWe0VK8G}vrg?-|>n0wS@5nAnD_Mgtm5uzUDFY^flg0W z^tzL&LOH1qW<6Yg!y~1-QUsC=b=CP`%U8etFwi*Oo|!6cA^zgND6K=>mTYEwNrr4c zwYKf2Bh&mHowdGfGJ<5b&&%%V1y-lrBCxi8NecF)gMhLp>2-y&{c;?=rHU)8zxUk$5?=XPbW^*0DiCj-*2kX1#Z;5O-)`xAmzoKM|p-^j4c=zn}NKGd(*C0TD9N zl1^G`r6rg>VBcLj^nA8kWD`Y`ieIsdIjJY~FJxEosZBZ#FxU4S7J@=C!#l%2%F=?drmne zF=1}>B6o_(M#arz5P>vySypjVYms@2R}@QO>GP>NLnb1SVHx1ZG`*k@URass$?=^8 zb*rEMOOTX!Mdmh z-B501G`Uw(UKYX?$CLv}HgTsC?{c?BD*PG)C^r)!7e`#kn7M-zx_XIElI=+El=dcNs9eF z*~AZVJd*MaiSlJX?C?o1Mi6!tJNR9nQ+hbQqL+}R`BPtyNVxanwKIDuk+HEaG)Jry-HApe)kWEuc^H@<}wt=?>2>Feclu33Eu z#+i`}SpGU3vZsxr@rOu_V@CLmC5lrjQp<%P{4=%E#Q!b({9SzIedFQ|Mc%fr7u2Q_ zu;d12prdfg{?Yq91j)4mX4cJWaP|P~dzkC#WJ`TifOkjh-Rv)PyFVEiasb(eM6WXI zch-1dX>-Nl&C}o}mv@b{53_IXV^Nj=i&9HsM5tX5{!Fh@idrL_n=@`-XxFq zCNe;9`l3x4PnAg;4IG#t^>s=5_8yF3E5056}_py$sb@kdGk>T_TR64y2d{`m~WN`k}yr^ z&nK0IlL1<5Fx@i8+HaJvSOHBzLXVJ9hg>n^ZM^=?<_Xdo_LxU+&Qk=V-muzVlxSc! z5&}LrVWV3yO=v?*7{_)n<8p^{~& z14WP{LjlEjeqr5eU>d3JdEIQLj-F2gWFJ$dUNcu8iTjMP^FibE5Wp=8o4$Vqa5p{x zm((xT2rv!uv7~~90H~htVgiip^vM5d9{hIty+=0P`X4e2AXCr>l!YG}-r7tle2c34 znBe|!F+E(NTOYky4$!MX$UA@Xc6TOAh5^)u@goWrSNXCjGXGKAS)uFBL_TxJ!{z+e zb#bPB0-xLNHvo^K_Psri|AP`WFeKFkA3>)pS`E8vt^Ov`*l@!)mvre%8+1UZL%D}M z^L|aY&6!=hzsiw2O91Xu4uJ2(Y+rGHFk|FvUT}_5bp)tPGrW{ZC4R-N@mv-xq3b+V z_kHMERDqOf%6CAKHvpO*tJ(`*2Qu7K0h&nv@p>H#aye^h zKA1pF!!$%Vv;d@toBjTt4s{}>>HU|(wlgKugO>9N`ItI4!1zSlE`FBrWxa}-Ttm=uGAh^=pF!)bbsDpH%_US=OFnIQ z+PO(@&uom%oMOWxRx$5t85q2Ex9a|jBuYI@trE2&IkYJbVyO&Ho2i%7Uwm#(k-H-p z%1mx%>|{6d9<=`Kxi^U-8p!mS4&6-$wg-Of#uoCkU{3HGg~nCy-nXPdVj);~!-y}& z@rZ{s(%H=wNUV3Kt6BUXZ)jWdsgd-H`uo$7S|88qW8adx!_c_f7fUdHj=GW&(9Ust zU)mafz=}#%+@8`?yJA9ba_=mx`|kHw8bzcSd5X7X9)f0)90yyhkPIkx2$@jdsCdv3 zK^Pz-N8c{~{_%O#ONBPuHQ@we0t*-*^Vi1|x;9!z8q1ImBXJI|!Nd!q>~A2_qRX17 zMR)+v@DTd5+;DdRTWD+J9iGYLEJCZtk$ne=nb#3?{S(L7VZ%-|$C!#_m)Kn2DDThr zf6ssn0@x0c7c}-vDH(<$dZOT7D|LZeRBI;k>DT8u4L2 z?9L~B;3wst_DKjCXhD+jy~fJ0$PnF50lIa{Ifuv{kWM4wga#eYkul$ds59TJQkE!Y z(%c<18(Ou{=w~Qkm8%s+-XK%}zXYi>?g>?Ju%DY7(0FawvHEd?P>C|DU6+ME;h2-u z+VQ1whzglHWlJeausi{*z&klP7wvb$_4^wa{paJLY*mmddH zya^epd*-*gMn>wmGNk<*`{wjHCA-wWyn#Aeuk(`v3^|ge^coUZre5;OK33?q^hWIy zqL8BS!};XsQ6V)BJ^8t%5RRB`BBtlWg1PDIKzw2!5{7Y&LCw%iU-5ELZBG~KdkZFA& zHno0?PafdA-`+gl9U)6bN@C;E5x_B6m*c%*by}0C@AmBazRy3D?Dr-^K!5)x_jg^R zz+W?31)Rr^M5Q9|;1sbn_#5CPL-%J~;;|TcdFE}=`EZ%}|EPQKaIXLNeK?|whAr89 zQzT@IY}vbz5y{?VNA_MBWeeHaM93zgY{?!W6xrR^^VNHO-k`lBDqv`6jZ%xNBUAZ&<-rQ6jRO}`K=Lv{7& zo~2VmIx8>PLiz zbqYVWF{_mJ~8QqZUnV2vJHs2H9mX5nd9lU2c- zncb-S%0;L+Rz2IWFjM#eLmiuA}nJ}~ZJRMk&Z$n5^;fLi2?!9H&y&+G`Q_|lnh1Yl@JVcY+pB|V5{ zN%-^f9vD~)Cg65#G=`6$N_haoD-BKgpBPhFgx^u+(;J>haU+U)UG-yaj5n@-m>h>?F@lp@ZK{o<(Pz;5t3!{X`Sj$Y*{N!4C6Yw9iL4b=C>cYQ z)#H?!zBt{fEA}Mfs?-dK6vRra5|mdL1n2RF?j#s|sR4f_(hMP&4he$Vv9pUg?V%Ty z*+O@`x%I2D*Z0YB2at;DeyaV`kHvTN?|SWnkv*Vg2Xr-Em7pu!`8tvqfJ=&$k-Z7= z*${gyvf@;Cp$sw|%p9!|Q&9itB|X>jkw`t^=;M2^P_a*Yd{@f*RlyuH>M*sZZRq() zC*7ug{rW!Ch3pCm9MMn+f1|aUg5t^64OD9~dl{C)7>5gqhP^*#L0-iUWpV8GLQlQ) z(_L#%=>=DF*{H ze`J~WCHwL4dLB5r&jgSnF0g12B*=k|Oo=)vCi#YQ!M@;S4-b_%72z`%C@hY03a2lM z-MM*^V%w;D{o$xlTo8BTo+K2TrbO5tuT&U7^T&XAw{V6)JY*eL0Y*98AqOY(TdA}DwaGS&9ewt@hzBlNVm{QxcTjeUe{Cfy*KjTaj-e}~cPx5^2Ol0=8 z>eCdrylyh!DMZNFLWuQ4{5>jVO=KQr&}_AKSWi^($rCiHAoisF6hX(_H88rVW!*Y| zvkFQ;wO3l4CL+zvk0Id3a^0!Bq(C4~s>OOpcwI+t58v$M8B^X7SD2bmbV-A`f6-<79a-7`O|W zxU@57+0Lwn@%2`XRjF40vCD+eHYx|CI8{`!u4=kAW0F2wVNA^^d*QM3N!$F!D)q0u zK=)ELf6r25Yl+g~Q8aOUab5AU^1*DmSQTNzd|D9uWgUUa%{;B-c&{r3fL@ZxxN*-- zd-};?OM-V>X`IXkUPO%k`I57i%+4lcElWLYwpDfM6E1{{8}*h>hQD7=h|E7K z3hIkoM=>hTHXqM6_Y+ZiJ1)KF^M=pHCBFYvTuwZJQ=g>35wvMcz3uisboVCSU8D6g z{3yr#+;n%{+okRTZbFgC{`LaXIMqZ&nT_?ElOsh&I{?LKctGb) zv%}Z#i3{yzwsS&G%SIgbItkAr5H(J?CXO`DD{?Uw&mPX|z1M-`v$v#H15)`NqdH5|Oh{O7L2k*A-uvy+a$OC*scCD5W#Drxj6mO7l9%m-|)~JdOEo&<_*m ztTTtE$Jj`vm9}G8d4O4nrDPm`YFBh-@tkSLN9)rI8NyfNZ+T|j3a9Jr1Rb_zl}Az_ z53%xQO6El^yCd_1enV3A6vcpTVtPMG>C3qa#%I|Z9i)6)FBXDgvmI?Z;j7KDS-pMm zWQXJ)1QQY#q!5)`*Z1w76)rXCT-@l;4P*LEL;0QpHbJ9m+Qa%Vw7{bDAzBaVm7+FM zR~?JenDosn&jY@k3ZHEybz-^hA>s-VB)Mc{Pd@aA@WZV;4y0eQt*bnB4Bv|c$D~RA zISb&YL6*)JpNQF~JWpGsP6(9+BoN7=`$D{Sm@PpFS;V^HlUlPgVUE5m^&c|RaF1`Q z5bS@)-Ky$4skv&>PK4FM-&?6Qc9N+xz3r|eP(l}}YwOeVu?WoNzzZR8{-`Pw zFpI14g%aML*d5hK>-y4@S{%lO0WS8gdLCR!|u{ znCY$KC4MSVx=;qj7W9Bw{Lt%U*fHntKe(`!7*O8Y`kC5>n9aL}lY4Sl8T;V-sAj|Q z%uII>iFuEa)z7R2IkSe!0%@z1@cxfp{w5?=(=7|(e7y?# z&CHLH#VB1I)xfJ_zV*ApdeqCAD3$o?4-NWT8+glcqDHPyx^qMheLijV<)}_gB^r=T zPl*hQpdQi#_&f`hB48@hdy}Z{qnXxcMp?YCZGi$Rx_3*-X-w}0l4;Tz@Jjm=(;I=5 zF%8R!O2^@$G-+;~f~wsouz*CH^e9qy zta1+99S@YL`bj^q<`|$*{NrW^{{TkC?+SSv8-vRv>S(p{%t7_wvQ^p&~ZyPykHE{VV$&rDV`K#(u6Ag79ANLS$Rz9 z2dTePB`R&ii5nxX1@lbW-(eOZ-XaLUB#PaNps@7HEn>N&iCMt%@ml0x|0k*vV^6mT zIG)=OsLZQ&?^n&PM6s!CeHg6x`y;dn-}bKn4|1)^@G-3TWX=x%4thpCUnJ3nk@Is^ zOiD1=L1_{NN@&9d>bn?gTDNEyY#RXcd`m%zB! ze(P&9*rCp^kn)fv7^6Lck{uc0fB+GgMqyJ$K}Z#8T7

kfP@pe7Z>LQ%vGvd?5|jPcDiw}K$G1aMCSp9)rgq<=88%l*P9<|mIIjq4Lig7 zs;y^%4O$iX-pku|G7zX3EoCp~>P_$|(tv#0DKc~z$NnyW5UkeY712md!K9Q-WRuYofM9X}awq9g zeo7N)y+z~rr-XvP#B7MFc+(x=TT;*msN3d7a$9SltSxb-=pB@th4XScNwhKP%Jp&7Iv2lz{cyBYVz^LOy>l9S%Fk}rB)(8R9SSwGhQ*0W zD`pOo8Yqv)0Ih}_s&YIxe{_F$ZOmNneQwE}dYBNWdegl3nemg}1e4rv1~Zp>R6!U% zAE&NtpVjM?ZAVw8V#I1SUXia+a`z5`^7gdN{1`$M>JDS_yreF7$j=;_yt^wTt86xs#Xd_i)uK9{l`ft941bEJRvG$L{h?ONRgxDxcynqzkvTUn)Nj zI#ILvYM9b!#0Y$2Hlk7ZsQjrChE*Kkl60QerUSRQO#KNTGWx3J{`c z78~g5l%P41tGluY01n_0RVu5fzWe6U;@KE`lGgnRoDPB5`j)Mh0Ng1W$FOId*hcsjyM{=*)Yl>p8_BH(4Crf1hgUk!%nUBAfe_>Glf>`HlU^~Qek#ya_#K}3$?f&&zL_e-_>&xdZdK|}mmYWs#_fO>>U$_0t zQ9b37@?&FaLJ_xIc?gjm55g>#9)YmeT14C3>?N0&U%RdHkhvar#B6!cng^_Oz{Zw` zv}moOSG_VDv#k8`6*VlX;Kjn^b)0nidX2AI1WR9*aFUhGQig_hxb3=c8qMm%;?j zi2>j10Q4?-&l$ss45>0F3DtREs;_Ciu5mrgQ*GMP7ARuV3`yJjA8}a{zKlwxi3pwrmD$uHv`fSifYuUzXi3 z#Z^`@k;X)L7`-z0^?qIlvI=|+nBFFS_oxU^xWKEkd_l|7ftY32oy!}7UwbK!_lbuk zJSeGax|Y7>Z;Y?@&SHu0e-C{njNf6eO4)I2=1380PR?am&@$M@IB=|>Eym;NLD;9; z^Sjf6i*}Aqf`IdLU;RYy?9A8^zv+(^UNRJhJh{oN~cND#k<$*R|YdyAMQ@#)6jCO1I^nq)sB&d-=xIB^O$ ze-rVlZ|9$UKuQ4)ppJley8)^7D{Kr4UV(g7o_oeWsq}gMSNO(E>@1>^&I)2AI z5U<5{{sT4qUNG`L(`^es*Dqgxs3R-a-P02bz^)5|0sVc0LML=nOkt@690V@pLkLwXx%)=aeg<8>0-eu#{2Dk+ijWqL-!7S)R-q0pl5^-I zg?S2Z9;}VM&H<;A8sJ+Me1R>(rtx>3Rl_+sVD0ntz!;7hay7qeG3KeM) z>T;tlpbIaNU$dY=@Co8&UAb|{+0qUe1I&qvpoJ9d?;~$g3u(L6xKVD%;52^2bK$ZY_ExfkDQq(Q- zbCMp~cZo^7_n*%4Iy0*p3{Evk?a+_3|09S8BSG9EAeQ-dLgL*=a)?RE{pXbH991Mj z>*)BGsz5!WT<7|Y3npfmWjHkcI(dvpXGj%F2&xq~ojgLid7HYX&^d%Dg5B+Y z3+9b8Psy{PcHp`94r?D;hdBV^aw+(gm^PUG6qtUn;~#gnZT0z=4O~Z+Sq=fnWk76? z8M*sdsXGxyvrT)tMzL&Cl+cw~ShlqZUrB zB+XYM7LVXkoad0m0~(f1$cS=X5tAXfKF={CIdjYS03N5L@tWOQE`8WaBN?x`>|A>| zGS~`(I0jM>Sq~BZM73>V^OLhZ&=}Qaxx<8s%9(L_Ui0I7xsMGhes^n&@jGu|7Sx2hBjhHG9%}TS|6kCb46#dax;(E zHvJxyX*Ny+ol)7h0H}d03s0#*n#*rJ{_#9d2bkH|@Y>77?)YN7Zw3uF1F+Bh5la$H zZ{|$pCT!Sh8D7WcugP2wx8+H)8bj^+7}@l9tSTN=og+>Hnfk^`L4b|rzc2P#M~(4# z@qYUXV+HFy`9E&TVy9r@FG{krsV$KT;#&nlBVk& zC5;CKun%v6*NDiGVx9nJvmi)fjUJtp&t?u^^#2KTFnqGKLO z4SmE=oJUHfu}c^b+(}G85x1SD=ts#IuO$j^yr=GWb^Tf_Zlle>95&r&Lc1;-6Hk&S z`#hMRA|1S~nlFOpYV!M=C)1b{BTy)aKJh*0dMFakXtc4`uD_m_kF20ZE)XHPU|GxZ zfTcR-oKEOJQbHtnrEB?f$Laq9OEg`esgUO};{4lI-YEh=XW{d+2a$g!9~NH*aG$ev z`BeTx9`X|lkk?y19vI#Qw-qUk3i-=>OvygK?@g8p*xmwQ6T4msN;Ccfpb>+6EPJi^ z-|3_@_AQi+v3`;-H)H%XasT#&J0u}jD_l3u|M%6hB3Fx}#fj2xpl%bK(Qvif%MYgi zzS?X&EGN3#^5J9}%OCUA&DO^&9YAf6VrU-mu>v}+oiKkb8^RF4gLug+sS1C5`FOuhj zeISG909?8tMM%7PK~U;sx4H@CCWIojSS~Qesr5X6XS&1_#CJSc?`lhU0)a^W=f)I* zaLUd5F6-9VWqAA;kQppDQ+Pey==s9GNRIuv9V?P z?<7bREUu+IbDmn%fa=jQEwmrQ%Z?`9=E8X_2eY4{O+w&R%U5Ge67{Sr<9O~otm^;~ ziL_e`I#w$qC22>ncei1%Q1@AC4;rAVD>?KkI#~=^WOcGjjcOhG_(q<203#IxJ6k>i zm~ThLWJtD`+Cu84#H=zLdgOMz=xVa-`es%Yq{LK;9j!fD%LcFfe$_0OeG4^EwrYq2 z^{HM5EA5w^#w}lV0h4}@>56G5UaS~2MdoYgB827vzLNg(3j@hR1atw^5p}{K-+r)kdgypJ^}j=LwZy@`LPWd`6sG>@kH_C;Vm!@-fP_d|RR^ z#8{5vK+>C3YS_-A{AHbq z7|_Yjx*b_PJE&jI44u!*ir^8|viYp>?PZJt&tXEhm+8b?dr-z@hK(Fdtec@%8A1HB;45=NQ(b??S|;_b3|@oO`$8X(P* zd*{QQJ&lQkvgDbA6K0^xL7cK2aiZzZtD481Ra;8eT{%(pmdYTOX%0G{(c4pF(Bv-@ zkLNZN-MDHpz5pAQU)>_^2~RtbKJ>=sflT2WvLYIPrOlmnD&q%DNyB!CZo;XTJ2c3o z!cE3YhBNDc_sd~m!qDCA-ZU{og#MV3vBOU=xx{N8mTK36y(sYf7h{S-!9l{Bm+9BN z+>oZ>EEEZYjZtnx$wJcTF&EC?`(p&+A@ybYT$|FpcXWu+TetC4ha*Ydd{12TI3~bSrRkltz@16lVB;o5DbfVBUy_OPZ);tn=u>a3NV$7t1xpsf^AVKW}23SmFrX zr!-pA%qiR!p?;M{&FH+(mxqbtn4|5Jx*0!M{a4|hjxo!Sj-!$K_FC;f{!YVp(DCo&r-E)z z02xc!DB0kzgs1|EVnq;@G*B!di?0BT6Cq~4qE!4B%wq96g(4PV6NekHoA zR{9|0uiPxoMTk`)zTnVNX$1AS()8~L+mp@KCuTmuIvZ{}qryN$z zc37C2ksHUI{i;P4?yTA|QuHTWq^U->R3*&KO@elvFo?JSv513?NZuf;+^mNLnlhoO zj_(B>pl>1zqA^n--;8^fA#EU&SzmY^SR$n1IWiuGxC;RVuHQZn<|>iHCbMAifUAK2U2R$e7oa1#VGQ4_u6m~bbn=G?ihAf-k6lc z&e>0v-Dp;-ez*8j()wt;UC9rH;N$N7LqVCx_HY3^^P1 z>MrNpwOuGY3)TeIACB1%O{uMBJbt+AZ}ViYju^9y>-}Dx^PhNnp2@fH2*(zJ!!1im zU&&G`&&}qu4qtrFwqKKHG3poo9yY(Qko2Sa^P^YS9(?RntgN*%ggRhSC;#&GNkKEn zC4T`bM4RBgR@RXXNN2IXXb*bx*N552himLj#!8K!h{UBIJXvknY{9*0#^A{OZSpmP z?XP3g4(DqBg08eHa@)VW0_X3z6$j%=OToKq44wZ}Y!=59Gym)GYF8THeb-e?+_s^6 zuGGwS+(EW%?fkxX1~PwFax^!wO>;0;0dia)W_~vN*nbDxN`<54$NN-4$KHx$CBu3b z>wK7Ta0JZR_0ETa<_CSmxrab((y~ai+@3Lomy`$~lNH0ALNys^e9hMu(D(9Yqa_Ft^u3V9j&UyH8S%zaL$3n3y6 zFVVNa`Fzq~o7S862T>oju#M6BG3j2VTL(C?)U;MP0V84A(6U*qzQVo_;gq0-VUX31Gt z_T6joq$1K7G26!bi~1yvs_x8bH12oZg`QsEe)dvLx~u+8-M)Hm@X?JdBM$YB1R71x>^1S={?nILHNZC2JyW7(qLRFdD+YH~{r{L(MNs^5e$5P(koz(3hV^+wI zw(Z`%e)h3>ZBYh8+f;D-2xpT(A$>UaQclS4SUxZ6TbR;f60(T&L`YbU670SJpBTE( z$3GorS%H)WN3kfvRm^Ib0~t76gF_sxIKAJrNl!iG7H#^9Un$^;5D5>Wgy7XG`PO)- zocYNx?uNBU+t{MFn63+j`{4Vgly3<~+Mlr~I|_x}&vy#Fc{T5zkb7iI@52AGu-c

ykr+v>S)08QC}DM9~Dj1f-Bh%0+>5 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 parseAndUpdateThreshold(n)); + + dimensionLive.textProperty().bind(dimensionLiveProperty); + dimensionStored.textProperty().bind(dimensionStoredProperty); + nonEqualCount.textProperty().bind(nonEqualCountProperty); } /** @@ -137,74 +159,60 @@ public void loadDataAndConnect(VType data, String pvName) { pvNameProperty.set(pvName); int arraySize = VTypeHelper.getArraySize(data); - if (data instanceof VNumberArray) { - for (int index = 0; index < arraySize; index++) { - List columnEntries = new ArrayList<>(); + for (int index = 0; index < arraySize; index++) { + List columnEntries = new ArrayList<>(); + ColumnEntry columnEntry = null; + if (data instanceof VNumberArray) { if (data instanceof VDoubleArray array) { double value = array.getData().getDouble(index); - ColumnEntry columnEntry = new ColumnEntry(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VFloatArray array) { float value = array.getData().getFloat(index); - ColumnEntry columnEntry = new ColumnEntry(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VIntArray array) { int value = array.getData().getInt(index); - ColumnEntry columnEntry = new ColumnEntry(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VUIntArray array) { int value = array.getData().getInt(index); - ColumnEntry columnEntry = new ColumnEntry(VUInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VUInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VLongArray array) { long value = array.getData().getLong(index); - ColumnEntry columnEntry = new ColumnEntry(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VULongArray array) { long value = array.getData().getLong(index); - ColumnEntry columnEntry = new ColumnEntry(VULong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VULong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VShortArray array) { short value = array.getData().getShort(index); - ColumnEntry columnEntry = new ColumnEntry(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VUShortArray array) { short value = array.getData().getShort(index); - ColumnEntry columnEntry = new ColumnEntry(VUShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VUShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VByteArray array) { byte value = array.getData().getByte(index); - ColumnEntry columnEntry = new ColumnEntry(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } else if (data instanceof VUByteArray array) { byte value = array.getData().getByte(index); - ColumnEntry columnEntry = new ColumnEntry(VUByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VUByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); } } - } else if (data instanceof VBooleanArray array) { - ListBoolean listBoolean = array.getData(); - for (int index = 0; index < listBoolean.size(); index++) { - List columnEntries = new ArrayList<>(); + else if (data instanceof VBooleanArray array) { + ListBoolean listBoolean = array.getData(); boolean value = listBoolean.getBoolean(index); - ColumnEntry columnEntry = new ColumnEntry(VBoolean.of(value, array.getAlarm(), array.getTime())); - addRow(index, columnEntries, columnEntry); + columnEntry = new ColumnEntry(VBoolean.of(value, array.getAlarm(), array.getTime())); + } else if (data instanceof VEnumArray array) { + List enumValues = array.getData(); + columnEntry = new ColumnEntry(VString.of(enumValues.get(index), array.getAlarm(), array.getTime())); + } else if (data instanceof VStringArray array) { + List stringValues = array.getData(); + columnEntry = new ColumnEntry(VString.of(stringValues.get(index), array.getAlarm(), array.getTime())); } - } else if (data instanceof VEnumArray array) { - List enumValues = array.getData(); - for (int index = 0; index < enumValues.size(); index++) { - List columnEntries = new ArrayList<>(); - ColumnEntry columnEntry = new ColumnEntry(VString.of(enumValues.get(index), array.getAlarm(), array.getTime())); - addRow(index, columnEntries, columnEntry); - } - } else if (data instanceof VStringArray array) { - List stringValues = array.getData(); - for (int index = 0; index < stringValues.size(); index++) { - List columnEntries = new ArrayList<>(); - ColumnEntry columnEntry = new ColumnEntry(VString.of(stringValues.get(index), array.getAlarm(), array.getTime())); + if(columnEntry != null){ addRow(index, columnEntries, columnEntry); } } + + // Hard coded column count until we support VTable + dimensionStoredProperty.set(arraySize + " x 1"); connect(); } @@ -299,103 +307,60 @@ private void updateTable(VType liveData) { // Live data may have more elements than stored data if (liveDataArraySize > comparisonTable.getItems().size()) { List columnEntries = new ArrayList<>(); - if (liveData instanceof VNumberArray) { - if (liveData instanceof VDoubleArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + if (liveData instanceof VNumberArray) { + if (liveData instanceof VDoubleArray array) { double value = array.getData().getDouble(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VDouble.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VFloatArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VFloatArray array) { float value = array.getData().getFloat(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VFloat.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VIntArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VIntArray array) { int value = array.getData().getInt(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VUIntArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VUIntArray array) { int value = array.getData().getInt(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VUInt.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VLongArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VLongArray array) { long value = array.getData().getLong(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VLong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VULongArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VULongArray array) { long value = array.getData().getLong(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VULong.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VShortArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VShortArray array) { short value = array.getData().getShort(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VUShortArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VUShortArray array) { short value = array.getData().getShort(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VUShort.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VByteArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VByteArray array) { byte value = array.getData().getByte(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); - } - } else if (liveData instanceof VUByteArray array) { - for (int index = comparisonTable.getItems().size(); index < liveDataArraySize; index++) { + } else if (liveData instanceof VUByteArray array) { byte value = array.getData().getByte(index); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); columnEntry.setLiveVal(VUByte.of(value, array.getAlarm(), array.getTime(), array.getDisplay())); - addRow(index, columnEntries, columnEntry); } } - } else if (liveData instanceof VBooleanArray array) { - ListBoolean listBoolean = array.getData(); - for (int i = 0; i < listBoolean.size(); i++) { - boolean value = listBoolean.getBoolean(i); - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); + else if (liveData instanceof VBooleanArray array) { + ListBoolean listBoolean = array.getData(); + boolean value = listBoolean.getBoolean(index); columnEntry.setLiveVal(VBoolean.of(value, array.getAlarm(), array.getTime())); - addRow(i, columnEntries, columnEntry); - } - } else if (liveData instanceof VEnumArray array) { - List enumValues = array.getData(); - for (int i = 0; i < enumValues.size(); i++) { - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VString.of(enumValues.get(i), array.getAlarm(), array.getTime())); - addRow(i, columnEntries, columnEntry); - } - } else if (liveData instanceof VStringArray array) { - List stringValues = array.getData(); - for (int i = 0; i < stringValues.size(); i++) { - ColumnEntry columnEntry = new ColumnEntry(VNoData.INSTANCE); - columnEntry.setLiveVal(VString.of(stringValues.get(i), array.getAlarm(), array.getTime())); - addRow(i, columnEntries, columnEntry); + } else if (liveData instanceof VEnumArray array) { + List enumValues = array.getData(); + columnEntry.setLiveVal(VString.of(enumValues.get(index), array.getAlarm(), array.getTime())); + } else if (liveData instanceof VStringArray array) { + List stringValues = array.getData(); + columnEntry.setLiveVal(VString.of(stringValues.get(index), array.getAlarm(), array.getTime())); } + addRow(index, columnEntries, columnEntry); } } + // Hard coded column count until we support VTable + dimensionLiveProperty.set(liveDataArraySize + " x 1"); + computeNonEqualCount(); } + } private void parseAndUpdateThreshold(String value) { @@ -418,8 +383,24 @@ private void parseAndUpdateThreshold(String value) { */ private void updateThreshold(double threshold) { double ratio = threshold / 100; + comparisonTable.getItems().forEach(comparisonData -> { comparisonData.setThreshold(ratio); }); + + computeNonEqualCount(); + } + + private void computeNonEqualCount(){ + AtomicInteger nonEqualCount = new AtomicInteger(0); + comparisonTable.getItems().forEach(comparisonData -> { + comparisonData.getColumnEntries().forEach(columnEntry -> { + if(!Utilities.areValuesEqual(columnEntry.liveValueProperty().get(), columnEntry.storedValueProperty().get(), columnEntry.getDelta().get().threshold)){ + nonEqualCount.incrementAndGet(); + } + }); + }); + + nonEqualCountProperty.set(nonEqualCount.toString()); } } diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 7070d4f03f..12f05b08bb 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -74,6 +74,8 @@ deleteFilter=Delete Filter deleteFilterFailed=Failed to delete filter description=Description descriptionHint=Specify non-empty description +dimensionLive=Dimension Live +dimensionStored=Dimension Stored duplicatePVNamesAdditionalItems={0} additional PV names duplicatePVNamesCheckFailed=Cannot check if snapshots may be added, remote service off-line? duplicatePVNamesFoundInSelection=Cannot add selected snapshots, duplicate PV names found @@ -145,6 +147,7 @@ noValueAvailable=No Value Available help=Help hitsPerPage=Hits per page nodeSelectionForConfiguration=Select Node +nonEqualCount=Non-equal count openResourceFailedTitle=Unable To Open openResourceFailedHeader=Node with id "{0}" does not exist openSearchView=Open Search View diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml index 19099a9ca9..b0d0e9cb9b 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/snapshot/compare/TableComparisonView.fxml @@ -1,23 +1,66 @@ - - - - + + + + + + + + + + - - + - - - - + + +

+ + + +
+ +
@@ -28,9 +71,9 @@ - - - + + + diff --git a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java index cb945566a0..2781bd3695 100644 --- a/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java +++ b/app/save-and-restore/app/src/test/java/org/phoebus/applications/saveandrestore/ui/snapshot/compare/ComparisonDialogDemo.java @@ -26,11 +26,11 @@ public static void main(String[] args) { public void start(Stage primaryStage) { VDoubleArray vDoubleArray = - VDoubleArray.of(ArrayDouble.of(1, 2, 3), + VDoubleArray.of(ArrayDouble.of(1, 2, 3, 4), Alarm.none(), Time.now(), Display.none()); ComparisonDialog comparisonDialog = - new ComparisonDialog(vDoubleArray, "loc://x(3, 2, 1, 1)"); + new ComparisonDialog(vDoubleArray, "loc://x(3, 2, 1)"); comparisonDialog.show(); } } From d2fba8be032096a14274fb51f9eefd3e9672afa2 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 10 Dec 2025 15:45:39 +0100 Subject: [PATCH 23/23] Do not launch comparison dialog if live PV is not connected --- .../saveandrestore/ui/snapshot/VDeltaCellEditor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java index e072c002d0..faae91e738 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/VDeltaCellEditor.java @@ -96,6 +96,8 @@ public void updateItem(T item, boolean empty) { comparisonDialog.show(); }); } else { + // Do not handle mouse clicked, e.g. if live PV is disconnected. + setOnMouseClicked(e -> {}); String percentage = Utilities.deltaValueToPercentage(pair.value, pair.base); if (!percentage.isEmpty() && showDeltaPercentage) { Formatter formatter = new Formatter();