Java tutorial
/*- * #%L * This file is part of QuPath. * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ package qupath.lib.gui.tma; import java.awt.image.BufferedImage; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Scanner; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.script.SimpleBindings; import org.apache.commons.math3.stat.correlation.PearsonsCorrelation; import org.apache.commons.math3.stat.correlation.SpearmansCorrelation; import org.controlsfx.control.MasterDetailPane; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.textfield.TextFields; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.geometry.Side; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.ScatterChart; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane.ScrollBarPolicy; import javafx.scene.control.SelectionMode; import javafx.scene.control.Separator; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TabPane.TabClosingPolicy; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn.CellDataFeatures; import javafx.scene.control.TreeTableRow; import javafx.scene.control.TreeTableView; import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.effect.DropShadow; import javafx.scene.input.Clipboard; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.stage.Stage; import javafx.util.Callback; import qupath.lib.common.GeneralTools; import qupath.lib.gui.QuPathGUI; import qupath.lib.gui.commands.SummaryMeasurementTableCommand; import qupath.lib.gui.helpers.ChartToolsFX; import qupath.lib.gui.helpers.DisplayHelpers; import qupath.lib.gui.helpers.PanelToolsFX; import qupath.lib.gui.models.HistogramDisplay; import qupath.lib.gui.models.ObservableMeasurementTableData; import qupath.lib.gui.models.PathTableData; import qupath.lib.gui.panels.survival.KaplanMeierDisplay; import qupath.lib.gui.prefs.PathPrefs; import qupath.lib.gui.tma.cells.BasicTableCell; import qupath.lib.gui.tma.cells.ImageListCell; import qupath.lib.gui.tma.cells.ImageTableCell; import qupath.lib.gui.tma.cells.NumericTableCell; import qupath.lib.gui.tma.entries.DefaultTMAEntry; import qupath.lib.gui.tma.entries.TMAEntry; import qupath.lib.gui.tma.entries.TMAImageCache; import qupath.lib.gui.tma.entries.TMAObjectEntry; import qupath.lib.gui.tma.entries.TMASummaryEntry; import qupath.lib.gui.tma.entries.TMASummaryEntry.MeasurementCombinationMethod; import qupath.lib.images.ImageData; import qupath.lib.io.PathIO; import qupath.lib.io.TMAScoreImporter; import qupath.lib.measurements.MeasurementList; import qupath.lib.objects.TMACoreObject; import qupath.lib.objects.hierarchy.DefaultTMAGrid; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.objects.hierarchy.TMAGrid; import qupath.lib.projects.Project; import qupath.lib.projects.ProjectImageEntry; /** * Standalone viewer for looking at TMA summary results. * * @author Pete Bankhead * */ public class TMASummaryViewer { public final static Logger logger = LoggerFactory.getLogger(TMASummaryViewer.class); private IntegerProperty maxSmallWidth = new SimpleIntegerProperty(150); private TMAImageCache imageCache = new TMAImageCache(maxSmallWidth.get()); private static String MISSING_COLUMN = "Missing"; private final Stage stage; private ObservableList<String> metadataNames = FXCollections.observableArrayList(); private ObservableList<String> measurementNames = FXCollections.observableArrayList(); private ObservableList<String> filteredMeasurementNames = new FilteredList<>(measurementNames, m -> !TMASummaryEntry.isSurvivalColumn(m)); private ObservableList<String> survivalColumns = FXCollections.observableArrayList(); private ComboBox<String> comboSurvival = new ComboBox<>(survivalColumns); private ObservableList<TMAEntry> entriesBase = FXCollections.observableArrayList(); /** * If trimUniqueIDs is true, Unique ID string will be trimmed to remove whitespace. * This can help with alignment problems due to the ID containing (unrecognized) additional spaces. */ private boolean trimUniqueIDs = true; /** * Maintain a reference to columns that were previously hidden whenever loading new data. * This helps maintain some continuity, so that if any columns have the same names then they * can be hidden as well. */ private Set<String> lastHiddenColumns = new HashSet<>(); private String colCensored = null; private Scene scene; private static enum ImageAvailability { IMAGE_ONLY, OVERLAY_ONLY, BOTH, NONE } private static ObjectProperty<ImageAvailability> imageAvailability = new SimpleObjectProperty<>( ImageAvailability.NONE); /** * A combo-box representing the main measurement. * This will be used for any survival curves. */ private ComboBox<String> comboMainMeasurement = new ComboBox<>(filteredMeasurementNames); /** * A combo-box representing how measurements are combined whenever multiple cores are available per patient. * * Options include min, max, mean & median. */ private ComboBox<MeasurementCombinationMethod> comboMeasurementMethod = new ComboBox<>(); private ReadOnlyObjectProperty<MeasurementCombinationMethod> selectedMeasurementCombinationProperty = comboMeasurementMethod .getSelectionModel().selectedItemProperty(); private TreeTableView<TMAEntry> table = new TreeTableView<>(); private TMATableModel model; private TMAEntry entrySelected = null; private BooleanProperty hidePaneProperty = new SimpleBooleanProperty(false); private BooleanProperty useSelectedProperty = new SimpleBooleanProperty(false); private BooleanProperty skipMissingCoresProperty = new SimpleBooleanProperty(true); private BooleanProperty groupByIDProperty = new SimpleBooleanProperty(true); private HistogramDisplay histogramDisplay; private KaplanMeierDisplay kmDisplay; private ScatterPane scatterPane = new ScatterPane(); private ObservableValue<Predicate<TMAEntry>> predicateHideMissing = Bindings.createObjectBinding(() -> { if (!skipMissingCoresProperty.get()) return c -> true; else return c -> !c.isMissing(); }, skipMissingCoresProperty); private ObjectProperty<Predicate<TMAEntry>> predicateMetadataFilter = new SimpleObjectProperty<>(); private ObjectProperty<Predicate<TMAEntry>> predicateMeasurements = new SimpleObjectProperty<>(); private ObservableValue<Predicate<TMAEntry>> combinedPredicate; public TMASummaryViewer(final Stage stage) { if (stage == null) this.stage = new Stage(); else this.stage = stage; combinedPredicate = Bindings.createObjectBinding(() -> { Predicate<TMAEntry> thisPredicate = predicateHideMissing.getValue(); if (predicateMeasurements.get() != null) thisPredicate = thisPredicate.and(predicateMeasurements.getValue()); if (predicateMetadataFilter.get() != null) thisPredicate = thisPredicate.and(predicateMetadataFilter.getValue()); return thisPredicate; }, predicateMeasurements, predicateHideMissing, predicateMetadataFilter); initialize(); this.stage.setTitle("TMA Results Viewer"); this.stage.setScene(scene); new DragDropTMADataImportListener(this); } private void initialize() { model = new TMATableModel(); groupByIDProperty.addListener((v, o, n) -> refreshTableData()); MenuBar menuBar = new MenuBar(); Menu menuFile = new Menu("File"); MenuItem miOpen = new MenuItem("Open..."); miOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.SHORTCUT_DOWN)); miOpen.setOnAction(e -> { File file = QuPathGUI.getDialogHelper(stage).promptForFile(null, null, "TMA data files", new String[] { "qptma" }); if (file == null) return; setInputFile(file); }); MenuItem miSave = new MenuItem("Save As..."); miSave.setAccelerator( new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); miSave.setOnAction( e -> SummaryMeasurementTableCommand.saveTableModel(model, null, Collections.emptyList())); MenuItem miImportFromImage = new MenuItem("Import from current image..."); miImportFromImage.setAccelerator( new KeyCodeCombination(KeyCode.I, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); miImportFromImage.setOnAction(e -> setTMAEntriesFromOpenImage()); MenuItem miImportFromProject = new MenuItem("Import from current project... (experimental)"); miImportFromProject.setAccelerator( new KeyCodeCombination(KeyCode.P, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); miImportFromProject.setOnAction(e -> setTMAEntriesFromOpenProject()); MenuItem miImportClipboard = new MenuItem("Import from clipboard..."); miImportClipboard.setOnAction(e -> { String text = Clipboard.getSystemClipboard().getString(); if (text == null) { DisplayHelpers.showErrorMessage("Import scores", "Clipboard is empty!"); return; } int n = importScores(text); if (n > 0) { setTMAEntries(new ArrayList<>(entriesBase)); } DisplayHelpers.showMessageDialog("Import scores", "Number of scores imported: " + n); }); Menu menuEdit = new Menu("Edit"); MenuItem miCopy = new MenuItem("Copy table to clipboard"); miCopy.setOnAction(e -> { SummaryMeasurementTableCommand.copyTableContentsToClipboard(model, Collections.emptyList()); }); combinedPredicate.addListener((v, o, n) -> { // We want any other changes triggered by this to have happened, // so that the data has already been updated Platform.runLater(() -> handleTableContentChange()); }); // Reset the scores for missing cores - this ensures they will be NaN and not influence subsequent results MenuItem miResetMissingScores = new MenuItem("Reset scores for missing cores"); miResetMissingScores.setOnAction(e -> { int changes = 0; for (TMAEntry entry : entriesBase) { if (!entry.isMissing()) continue; boolean changed = false; for (String m : entry.getMeasurementNames().toArray(new String[0])) { if (!TMASummaryEntry.isSurvivalColumn(m) && !Double.isNaN(entry.getMeasurementAsDouble(m))) { entry.putMeasurement(m, null); changed = true; } } if (changed) changes++; } if (changes == 0) { logger.info("No changes made when resetting scores for missing cores!"); return; } logger.info("{} change(s) made when resetting scores for missing cores!", changes); table.refresh(); updateSurvivalCurves(); if (scatterPane != null) scatterPane.updateChart(); if (histogramDisplay != null) histogramDisplay.refreshHistogram(); }); menuEdit.getItems().add(miResetMissingScores); QuPathGUI.addMenuItems(menuFile, miOpen, miSave, null, miImportClipboard, null, miImportFromImage, miImportFromProject); menuBar.getMenus().add(menuFile); menuEdit.getItems().add(miCopy); menuBar.getMenus().add(menuEdit); menuFile.setOnShowing(e -> { boolean imageDataAvailable = QuPathGUI.getInstance() != null && QuPathGUI.getInstance().getImageData() != null && QuPathGUI.getInstance().getImageData().getHierarchy().getTMAGrid() != null; miImportFromImage.setDisable(!imageDataAvailable); boolean projectAvailable = QuPathGUI.getInstance() != null && QuPathGUI.getInstance().getProject() != null && !QuPathGUI.getInstance().getProject().getImageList().isEmpty(); miImportFromProject.setDisable(!projectAvailable); }); // Double-clicking previously used for comments... but conflicts with tree table expansion // table.setOnMouseClicked(e -> { // if (!e.isPopupTrigger() && e.getClickCount() > 1) // promptForComment(); // }); table.setPlaceholder(new Text("Drag TMA data folder onto window, or choose File -> Open")); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); BorderPane pane = new BorderPane(); pane.setTop(menuBar); menuBar.setUseSystemMenuBar(true); // Create options ToolBar toolbar = new ToolBar(); Label labelMeasurementMethod = new Label("Combination method"); labelMeasurementMethod.setLabelFor(comboMeasurementMethod); labelMeasurementMethod .setTooltip(new Tooltip("Method whereby measurements for multiple cores with the same " + TMACoreObject.KEY_UNIQUE_ID + " will be combined")); CheckBox cbHidePane = new CheckBox("Hide pane"); cbHidePane.setSelected(hidePaneProperty.get()); cbHidePane.selectedProperty().bindBidirectional(hidePaneProperty); CheckBox cbGroupByID = new CheckBox("Group by ID"); entriesBase.addListener((Change<? extends TMAEntry> event) -> { if (!event.getList().stream().anyMatch(e -> e.getMetadataValue(TMACoreObject.KEY_UNIQUE_ID) != null)) { cbGroupByID.setSelected(false); cbGroupByID.setDisable(true); } else { cbGroupByID.setDisable(false); } }); cbGroupByID.setSelected(groupByIDProperty.get()); cbGroupByID.selectedProperty().bindBidirectional(groupByIDProperty); CheckBox cbUseSelected = new CheckBox("Use selection only"); cbUseSelected.selectedProperty().bindBidirectional(useSelectedProperty); CheckBox cbSkipMissing = new CheckBox("Hide missing cores"); cbSkipMissing.selectedProperty().bindBidirectional(skipMissingCoresProperty); skipMissingCoresProperty.addListener((v, o, n) -> { table.refresh(); updateSurvivalCurves(); if (histogramDisplay != null) histogramDisplay.refreshHistogram(); updateSurvivalCurves(); if (scatterPane != null) scatterPane.updateChart(); }); toolbar.getItems().addAll(labelMeasurementMethod, comboMeasurementMethod, new Separator(Orientation.VERTICAL), cbHidePane, new Separator(Orientation.VERTICAL), cbGroupByID, new Separator(Orientation.VERTICAL), cbUseSelected, new Separator(Orientation.VERTICAL), cbSkipMissing); comboMeasurementMethod.getItems().addAll(MeasurementCombinationMethod.values()); comboMeasurementMethod.getSelectionModel().select(MeasurementCombinationMethod.MEDIAN); selectedMeasurementCombinationProperty.addListener((v, o, n) -> table.refresh()); ContextMenu popup = new ContextMenu(); MenuItem miSetMissing = new MenuItem("Set missing"); miSetMissing.setOnAction(e -> setSelectedMissingStatus(true)); MenuItem miSetAvailable = new MenuItem("Set available"); miSetAvailable.setOnAction(e -> setSelectedMissingStatus(false)); MenuItem miExpand = new MenuItem("Expand all"); miExpand.setOnAction(e -> { if (table.getRoot() == null) return; for (TreeItem<?> item : table.getRoot().getChildren()) { item.setExpanded(true); } }); MenuItem miCollapse = new MenuItem("Collapse all"); miCollapse.setOnAction(e -> { if (table.getRoot() == null) return; for (TreeItem<?> item : table.getRoot().getChildren()) { item.setExpanded(false); } }); popup.getItems().addAll(miSetMissing, miSetAvailable, new SeparatorMenuItem(), miExpand, miCollapse); table.setContextMenu(popup); table.setRowFactory(e -> { TreeTableRow<TMAEntry> row = new TreeTableRow<>(); // // Make rows invisible if they don't pass the predicate // row.visibleProperty().bind(Bindings.createBooleanBinding(() -> { // TMAEntry entry = row.getItem(); // if (entry == null || (entry.isMissing() && skipMissingCoresProperty.get())) // return false; // return entries.getPredicate() == null || entries.getPredicate().test(entry); // }, // skipMissingCoresProperty, // entries.predicateProperty())); // Style rows according to what they contain row.styleProperty().bind(Bindings.createStringBinding(() -> { if (row.isSelected()) return ""; TMAEntry entry = row.getItem(); if (entry == null || entry instanceof TMASummaryEntry) return ""; else if (entry.isMissing()) return "-fx-background-color:rgb(225,225,232)"; else return "-fx-background-color:rgb(240,240,245)"; }, row.itemProperty(), row.selectedProperty())); // row.itemProperty().addListener((v, o, n) -> { // if (n == null || n instanceof TMASummaryEntry || row.isSelected()) // row.setStyle(""); // else if (n.isMissing()) // row.setStyle("-fx-background-color:rgb(225,225,232)"); // else // row.setStyle("-fx-background-color:rgb(240,240,245)"); // }); return row; }); BorderPane paneTable = new BorderPane(); paneTable.setTop(toolbar); paneTable.setCenter(table); MasterDetailPane mdTablePane = new MasterDetailPane(Side.RIGHT, paneTable, createSidePane(), true); mdTablePane.showDetailNodeProperty().bind(Bindings.createBooleanBinding( () -> !hidePaneProperty.get() && !entriesBase.isEmpty(), hidePaneProperty, entriesBase)); mdTablePane.setDividerPosition(2.0 / 3.0); pane.setCenter(mdTablePane); model.getEntries().addListener(new ListChangeListener<TMAEntry>() { @Override public void onChanged(ListChangeListener.Change<? extends TMAEntry> c) { if (histogramDisplay != null) histogramDisplay.refreshHistogram(); updateSurvivalCurves(); if (scatterPane != null) scatterPane.updateChart(); } }); Label labelPredicate = new Label(); labelPredicate.setPadding(new Insets(5, 5, 5, 5)); labelPredicate.setAlignment(Pos.CENTER); // labelPredicate.setStyle("-fx-background-color: rgba(20, 120, 20, 0.15);"); labelPredicate.setStyle("-fx-background-color: rgba(120, 20, 20, 0.15);"); labelPredicate.textProperty().addListener((v, o, n) -> { if (n.trim().length() > 0) pane.setBottom(labelPredicate); else pane.setBottom(null); }); labelPredicate.setMaxWidth(Double.MAX_VALUE); labelPredicate.setMaxHeight(labelPredicate.getPrefHeight()); labelPredicate.setTextAlignment(TextAlignment.CENTER); predicateMeasurements.addListener((v, o, n) -> { if (n == null) labelPredicate.setText(""); else if (n instanceof TablePredicate) { TablePredicate tp = (TablePredicate) n; if (tp.getOriginalCommand().trim().isEmpty()) labelPredicate.setText(""); else labelPredicate.setText("Predicate: " + tp.getOriginalCommand()); } else labelPredicate.setText("Predicate: " + n.toString()); }); // predicate.set(new TablePredicate("\"Tumor\" > 100")); scene = new Scene(pane); scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> { KeyCode code = e.getCode(); if ((code == KeyCode.SPACE || code == KeyCode.ENTER) && entrySelected != null) { promptForComment(); return; } }); } private void setSelectedMissingStatus(final boolean status) { for (TreeItem<TMAEntry> item : table.getSelectionModel().getSelectedItems()) { item.getValue().setMissing(status); } // Refresh the table data if necessary if (skipMissingCoresProperty.get()) { table.getSelectionModel().clearSelection(); refreshTableData(); } else table.refresh(); } /** * Update data due to a change in table content. */ private void handleTableContentChange() { table.refresh(); model.refreshList(); histogramDisplay.refreshHistogram(); updateSurvivalCurves(); scatterPane.updateChart(); } public Stage getStage() { return stage; } /** * Depending on the survival type, get the correct (unambiguous) column title for censoring. * * @param survivalColumn * @return */ static String getRequestedSurvivalCensoredColumn(final String survivalColumn) { if (TMACoreObject.KEY_OVERALL_SURVIVAL.equals(survivalColumn)) { return TMACoreObject.KEY_OS_CENSORED; } else if (TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL.equals(survivalColumn)) { return TMACoreObject.KEY_RFS_CENSORED; } return null; } private String getSurvivalColumn() { return comboSurvival.getSelectionModel().getSelectedItem(); } private void updateSurvivalCurves() { String colID = null; String colScore = null; colCensored = null; for (String nameOrig : model.getAllNames()) { if (nameOrig.equals(TMACoreObject.KEY_UNIQUE_ID)) colID = nameOrig; // else if (nameOrig.equals(TMACoreObject.KEY_CENSORED)) // colCensored = nameOrig; // else if (!Number.class.isAssignableFrom()) // continue; else { if (nameOrig.trim().length() == 0 || !model.getMeasurementNames().contains(nameOrig)) continue; String name = nameOrig.toLowerCase(); if (name.equals("h-score")) colScore = nameOrig; else if (name.equals("positive %") && colScore == null) colScore = nameOrig; } } // Check for a column with the exact requested name String colCensoredRequested = null; String colSurvival = getSurvivalColumn(); if (colSurvival != null) { colCensoredRequested = getRequestedSurvivalCensoredColumn(colSurvival); if (model.getAllNames().contains(colCensoredRequested)) colCensored = colCensoredRequested; // Check for a general 'censored' column... less secure since it doesn't specify OS or RFS (but helps with backwards-compatibility) else if (model.getAllNames().contains("Censored")) { logger.warn( "Correct censored column for \"{}\" unavailable - should be \"{}\", but using \"Censored\" column instead", colSurvival, colCensoredRequested); colCensored = "Censored"; } } if (colCensored == null && colSurvival != null) { logger.warn("Unable to find censored column - survival data will be uncensored"); } else logger.info("Survival column: {}, Censored column: {}", colSurvival, colCensored); colScore = comboMainMeasurement.getSelectionModel().getSelectedItem(); if (colID == null || colSurvival == null || colCensored == null) {// || colScore == null) { // Adjust priority depending on whether we have any data at all.. if (!model.getEntries().isEmpty()) logger.warn("No survival data found!"); else logger.trace("No entries or survival data available"); return; } // Generate a pseudo TMA core hierarchy Map<String, List<TMAEntry>> scoreMap = createScoresMap(model.getEntries(), colScore, colID); // System.err.println("Score map size: " + scoreMap.size() + "\tEntries: " + model.getEntries().size()); List<TMACoreObject> cores = new ArrayList<>(scoreMap.size()); double[] scores = new double[15]; for (Entry<String, List<TMAEntry>> entry : scoreMap.entrySet()) { TMACoreObject core = new TMACoreObject(); core.setName("ID: " + entry.getKey()); MeasurementList ml = core.getMeasurementList(); Arrays.fill(scores, Double.POSITIVE_INFINITY); List<TMAEntry> list = entry.getValue(); // Increase array size, if needed if (list.size() > scores.length) scores = new double[list.size()]; for (int i = 0; i < list.size(); i++) { scores[i] = model.getNumericValue(list.get(i), colScore); // scores[i] = list.get(i).getMeasurement(colScore).doubleValue(); } Arrays.sort(scores); int n = list.size(); double score; if (n % 2 == 1) score = scores[n / 2]; else score = (scores[n / 2 - 1] + scores[n / 2]) / 2; core.putMetadataValue(TMACoreObject.KEY_UNIQUE_ID, entry.getKey()); // System.err.println("Putting: " + list.get(0).getMeasurement(colSurvival).doubleValue() + " LIST: " + list.size()); ml.putMeasurement(colSurvival, list.get(0).getMeasurementAsDouble(colSurvival)); ml.putMeasurement(colCensoredRequested, list.get(0).getMeasurementAsDouble(colCensored)); if (colScore != null) ml.putMeasurement(colScore, score); cores.add(core); // logger.info(entry.getKey() + "\t" + score); } TMAGrid grid = new DefaultTMAGrid(cores, 1); PathObjectHierarchy hierarchy = new PathObjectHierarchy(); hierarchy.setTMAGrid(grid); kmDisplay.setHierarchy(hierarchy, colSurvival, colCensoredRequested); kmDisplay.setScoreColumn(comboMainMeasurement.getSelectionModel().getSelectedItem()); // new KaplanMeierPlotTMA.KaplanMeierDisplay(hierarchy, colScore).show(frame, colScore); } private Pane createSidePane() { BorderPane pane = new BorderPane(); TabPane tabPane = new TabPane(); kmDisplay = new KaplanMeierDisplay(null, null, null, null); BorderPane paneKaplanMeier = new BorderPane(); paneKaplanMeier.setCenter(kmDisplay.getView()); paneKaplanMeier.setPadding(new Insets(10, 10, 10, 10)); // comboMainMeasurement.prefWidthProperty().bind(paneKaplanMeier.widthProperty()); comboMainMeasurement.setMaxWidth(Double.MAX_VALUE); comboMainMeasurement.setTooltip(new Tooltip("Measurement thresholded to create survival curves etc.")); GridPane kmTop = new GridPane(); kmTop.add(new Label("Score"), 0, 0); kmTop.add(comboMainMeasurement, 1, 0); kmTop.add(new Label("Survival type"), 0, 1); kmTop.add(comboSurvival, 1, 1); comboSurvival.setTooltip(new Tooltip("Specify overall or recurrence-free survival (if applicable)")); comboSurvival.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(comboMainMeasurement, Priority.ALWAYS); GridPane.setHgrow(comboSurvival, Priority.ALWAYS); kmTop.setHgap(5); paneKaplanMeier.setTop(kmTop); // kmDisplay.setOrientation(Orientation.VERTICAL); histogramDisplay = new HistogramDisplay(model, false); comboMainMeasurement.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { histogramDisplay.refreshCombo(); histogramDisplay.showHistogram(n); updateSurvivalCurves(); }); comboMeasurementMethod.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { histogramDisplay.refreshHistogram(); scatterPane.updateChart(); updateSurvivalCurves(); }); comboSurvival.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { updateSurvivalCurves(); }); // Create a Tab for showing images BorderPane paneImages = new BorderPane(); CheckBox cbShowOverlay = new CheckBox("Show overlay"); imageAvailability.addListener((c, v, n) -> { if (n == ImageAvailability.OVERLAY_ONLY) cbShowOverlay.setSelected(true); else if (n == ImageAvailability.IMAGE_ONLY) cbShowOverlay.setSelected(false); cbShowOverlay.setDisable(n != ImageAvailability.BOTH); }); ListView<TMAEntry> listImages = new ListView<>(); listImages.setCellFactory(v -> new ImageListCell(cbShowOverlay.selectedProperty(), imageCache)); listImages.widthProperty().addListener((v, o, n) -> listImages.refresh()); listImages.setStyle("-fx-control-inner-background-alt: -fx-control-inner-background ;"); table.getSelectionModel().getSelectedItems().addListener((Change<? extends TreeItem<TMAEntry>> e) -> { List<TMAEntry> entries = new ArrayList<>(); for (TreeItem<TMAEntry> item : e.getList()) { if (item.getChildren().isEmpty()) { if (item.getValue().hasImage() || item.getValue().hasOverlay()) entries.add(item.getValue()); } else { for (TreeItem<TMAEntry> item2 : item.getChildren()) { if (item2.getValue().hasImage() || item2.getValue().hasOverlay()) entries.add(item2.getValue()); } } listImages.getItems().setAll(entries); } }); cbShowOverlay.setAlignment(Pos.CENTER); cbShowOverlay.setMaxWidth(Double.MAX_VALUE); cbShowOverlay.setPadding(new Insets(5, 5, 5, 5)); cbShowOverlay.selectedProperty().addListener((v, o, n) -> listImages.refresh()); paneImages.setCenter(listImages); paneImages.setTop(cbShowOverlay); // Determine visibility based upon whether there are any images to show // Tab tabImages = new Tab("Images", paneImages); ScrollPane scrollPane = new ScrollPane(paneKaplanMeier); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); Tab tabSurvival = new Tab("Survival", scrollPane); tabPane.getTabs().addAll(new Tab("Table", getCustomizeTablePane()), // tabImages, new Tab("Histogram", histogramDisplay.getPane()), new Tab("Scatterplot", scatterPane.getPane()), tabSurvival); tabPane.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE); // if (imageAvailability.get() != ImageAvailability.NONE) // tabPane.getTabs().add(1, tabImages); // // imageAvailability.addListener((c, v, n) -> { // if (n == ImageAvailability.NONE) // tabPane.getTabs().remove(tabImages); // else if (!tabPane.getTabs().contains(tabImages)) // tabPane.getTabs().add(1, tabImages); // }); // tabSurvival.visibleProperty().bind( // Bindings.createBooleanBinding(() -> !survivalColumns.isEmpty(), survivalColumns) // ); pane.setCenter(tabPane); pane.setMinWidth(350); return pane; } private Pane getCustomizeTablePane() { TableView<TreeTableColumn<TMAEntry, ?>> tableColumns = new TableView<>(); tableColumns.setPlaceholder(new Text("No columns available")); tableColumns.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); tableColumns.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); SortedList<TreeTableColumn<TMAEntry, ?>> sortedColumns = new SortedList<>( table.getColumns().filtered(p -> !p.getText().trim().isEmpty())); sortedColumns.setComparator((c1, c2) -> c1.getText().compareTo(c2.getText())); tableColumns.setItems(sortedColumns); sortedColumns.comparatorProperty().bind(tableColumns.comparatorProperty()); // sortedColumns.comparatorProperty().bind(tableColumns.comparatorProperty()); TableColumn<TreeTableColumn<TMAEntry, ?>, String> columnName = new TableColumn<>("Column"); columnName.setCellValueFactory(v -> v.getValue().textProperty()); TableColumn<TreeTableColumn<TMAEntry, ?>, Boolean> columnVisible = new TableColumn<>("Visible"); columnVisible.setCellValueFactory(v -> v.getValue().visibleProperty()); // columnVisible.setCellValueFactory(col -> { // SimpleBooleanProperty prop = new SimpleBooleanProperty(col.getValue().isVisible()); // prop.addListener((v, o, n) -> col.getValue().setVisible(n)); // return prop; // }); tableColumns.setEditable(true); columnVisible.setCellFactory(v -> new CheckBoxTableCell<>()); tableColumns.getColumns().add(columnName); tableColumns.getColumns().add(columnVisible); ContextMenu contextMenu = new ContextMenu(); Action actionShowSelected = new Action("Show selected", e -> { for (TreeTableColumn<?, ?> col : tableColumns.getSelectionModel().getSelectedItems()) { if (col != null) col.setVisible(true); else { // Not sure why this happens...? logger.trace("Selected column is null!"); } } }); Action actionHideSelected = new Action("Hide selected", e -> { for (TreeTableColumn<?, ?> col : tableColumns.getSelectionModel().getSelectedItems()) { if (col != null) col.setVisible(false); else { // Not sure why this happens...? logger.trace("Selected column is null!"); } } }); contextMenu.getItems().addAll(ActionUtils.createMenuItem(actionShowSelected), ActionUtils.createMenuItem(actionHideSelected)); tableColumns.setContextMenu(contextMenu); tableColumns.setTooltip( new Tooltip("Show or hide table columns - right-click to change multiple columns at once")); BorderPane paneColumns = new BorderPane(tableColumns); paneColumns.setBottom(PanelToolsFX.createColumnGridControls(ActionUtils.createButton(actionShowSelected), ActionUtils.createButton(actionHideSelected))); VBox paneRows = new VBox(); // Create a box to filter on some metadata text ComboBox<String> comboMetadata = new ComboBox<>(); comboMetadata.setItems(metadataNames); comboMetadata.getSelectionModel().getSelectedItem(); comboMetadata.setPromptText("Select column"); TextField tfFilter = new TextField(); CheckBox cbExact = new CheckBox("Exact"); // Set listeners cbExact.selectedProperty().addListener( (v, o, n) -> setMetadataTextPredicate(comboMetadata.getSelectionModel().getSelectedItem(), tfFilter.getText(), cbExact.isSelected(), !cbExact.isSelected())); tfFilter.textProperty().addListener( (v, o, n) -> setMetadataTextPredicate(comboMetadata.getSelectionModel().getSelectedItem(), tfFilter.getText(), cbExact.isSelected(), !cbExact.isSelected())); comboMetadata.getSelectionModel().selectedItemProperty().addListener( (v, o, n) -> setMetadataTextPredicate(comboMetadata.getSelectionModel().getSelectedItem(), tfFilter.getText(), cbExact.isSelected(), !cbExact.isSelected())); GridPane paneMetadata = new GridPane(); paneMetadata.add(comboMetadata, 0, 0); paneMetadata.add(tfFilter, 1, 0); paneMetadata.add(cbExact, 2, 0); paneMetadata.setPadding(new Insets(10, 10, 10, 10)); paneMetadata.setVgap(2); paneMetadata.setHgap(5); comboMetadata.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(tfFilter, Priority.ALWAYS); GridPane.setFillWidth(comboMetadata, Boolean.TRUE); GridPane.setFillWidth(tfFilter, Boolean.TRUE); TitledPane tpMetadata = new TitledPane("Metadata filter", paneMetadata); tpMetadata.setExpanded(false); // tpMetadata.setCollapsible(false); Tooltip tooltipMetadata = new Tooltip( "Enter text to filter entries according to a selected metadata column"); Tooltip.install(paneMetadata, tooltipMetadata); tpMetadata.setTooltip(tooltipMetadata); paneRows.getChildren().add(tpMetadata); // Add measurement predicate TextField tfCommand = new TextField(); tfCommand.setTooltip(new Tooltip("Predicate used to filter entries for inclusion")); TextFields.bindAutoCompletion(tfCommand, e -> { int ind = tfCommand.getText().lastIndexOf("\""); if (ind < 0) return Collections.emptyList(); String part = tfCommand.getText().substring(ind + 1); return measurementNames.stream().filter(n -> n.startsWith(part)).map(n -> "\"" + n + "\" ") .collect(Collectors.toList()); }); String instructions = "Enter a predicate to filter entries.\n" + "Only entries passing the test will be included in any results.\n" + "Examples of predicates include:\n" + " \"Num Tumor\" > 200\n" + " \"Num Tumor\" > 100 && \"Num Stroma\" < 1000"; // labelInstructions.setTooltip(new Tooltip("Note: measurement names must be in \"inverted commands\" and\n" + // "&& indicates 'and', while || indicates 'or'.")); BorderPane paneMeasurementFilter = new BorderPane(tfCommand); Label label = new Label("Predicate: "); label.setAlignment(Pos.CENTER); label.setMaxHeight(Double.MAX_VALUE); paneMeasurementFilter.setLeft(label); Button btnApply = new Button("Apply"); btnApply.setOnAction(e -> { TablePredicate predicateNew = new TablePredicate(tfCommand.getText()); if (predicateNew.isValid()) { predicateMeasurements.set(predicateNew); } else { DisplayHelpers.showErrorMessage("Invalid predicate", "Current predicate '" + tfCommand.getText() + "' is invalid!"); } e.consume(); }); TitledPane tpMeasurementFilter = new TitledPane("Measurement filter", paneMeasurementFilter); tpMeasurementFilter.setExpanded(false); Tooltip tooltipInstructions = new Tooltip(instructions); tpMeasurementFilter.setTooltip(tooltipInstructions); Tooltip.install(paneMeasurementFilter, tooltipInstructions); paneMeasurementFilter.setRight(btnApply); paneRows.getChildren().add(tpMeasurementFilter); logger.info("Predicate set to: {}", predicateMeasurements.get()); VBox pane = new VBox(); // TitledPane tpColumns = new TitledPane("Select column", paneColumns); // tpColumns.setMaxHeight(Double.MAX_VALUE); // tpColumns.setCollapsible(false); pane.getChildren().addAll(paneColumns, new Separator(), paneRows); VBox.setVgrow(paneColumns, Priority.ALWAYS); return pane; } /** * Set a filter based on a (single) metadata column. * * @param metadataName * @param filterText * @param exact * @param ignoreCase */ private void setMetadataTextPredicate(final String metadataName, final String filterText, final boolean exact, final boolean ignoreCase) { if (metadataName == null || filterText == null || metadataName.trim().isEmpty() || filterText.trim().isEmpty()) { predicateMetadataFilter.set(null); } else { if (ignoreCase) { String filterTextLower = filterText.toLowerCase(); if (exact) predicateMetadataFilter.set(t -> t.getMetadataValue(metadataName) != null && t.getMetadataValue(metadataName).toLowerCase().equals(filterTextLower)); else predicateMetadataFilter.set(t -> t.getMetadataValue(metadataName) != null && t.getMetadataValue(metadataName).toLowerCase().contains(filterTextLower)); } else if (exact) predicateMetadataFilter.set(t -> t.getMetadataValue(metadataName) != null && t.getMetadataValue(metadataName).equals(filterText)); else predicateMetadataFilter.set(t -> t.getMetadataValue(metadataName) != null && t.getMetadataValue(metadataName).contains(filterText)); } } private Map<String, List<TMAEntry>> createScoresMap(final List<TMAEntry> entries, final String colScore, final String colID) { // Create a map of entries Map<String, List<TMAEntry>> scoreMap = new HashMap<>(); for (TMAEntry entry : entries) { Number score = model.getNumericValue(entry, colScore); String id = entry.getMetadataValue(colID); if (id == null && entry.getMeasurement(colID) != null) id = Double.toString(entry.getMeasurement(colID).doubleValue()); if (id != null && score != null && !Double.isNaN(score.doubleValue())) { List<TMAEntry> list = scoreMap.get(id); if (list == null) { list = new ArrayList<>(); scoreMap.put(id, list); } list.add(entry); } } return scoreMap; } private void setTMAEntriesFromOpenImage() { QuPathGUI qupath = QuPathGUI.getInstance(); if (qupath == null || qupath.getImageData() == null || qupath.getImageData().getHierarchy().getTMAGrid() == null) { DisplayHelpers.showErrorMessage("Show TMA summary", "No TMA data available!"); return; } ImageData<BufferedImage> imageData = qupath.getImageData(); setTMAEntriesFromImageData(imageData); } private void setTMAEntriesFromOpenProject() { QuPathGUI qupath = QuPathGUI.getInstance(); if (qupath == null || qupath.getProject() == null || qupath.getProject().getImageList().isEmpty()) { DisplayHelpers.showErrorMessage("Show TMA summary", "No project available!"); return; } Project<BufferedImage> project = qupath.getProject(); List<TMAEntry> entries = new ArrayList<>(); for (ProjectImageEntry<BufferedImage> imageEntry : project.getImageList()) { File file = QuPathGUI.getImageDataFile(project, imageEntry); if (file.isFile()) { logger.info("Reading from {}", file); ImageData<BufferedImage> imageData = PathIO.readImageData(file, null, null, BufferedImage.class); if (imageData != null) entries.addAll(getEntriesForTMAData(imageData)); else logger.error("No ImageData read for {]", file); } } setTMAEntries(entries); stage.setTitle("TMA Viewer: " + project.getName()); } private static List<TMAEntry> getEntriesForTMAData(final ImageData<BufferedImage> imageData) { List<TMAEntry> entriesNew = new ArrayList<>(); if (imageData.getHierarchy().getTMAGrid() == null) return entriesNew; ObservableMeasurementTableData data = new ObservableMeasurementTableData(); data.setImageData(imageData, imageData.getHierarchy().getTMAGrid().getTMACoreList()); for (TMACoreObject core : imageData.getHierarchy().getTMAGrid().getTMACoreList()) { entriesNew.add(new TMAObjectEntry(imageData, data, core)); } return entriesNew; } /** * Set the TMA entries from the TMACoreObjects of a specific ImageData. * * @param imageData */ public void setTMAEntriesFromImageData(final ImageData<BufferedImage> imageData) { setTMAEntries(getEntriesForTMAData(imageData)); stage.setTitle("TMA Viewer: " + imageData.getServer().getShortServerName()); } public void setInputFile(File file) { if (file == null) return; if (file.getName().toLowerCase().endsWith(PathPrefs.getSerializationExtension())) { ImageData<BufferedImage> imageData = PathIO.readImageData(file, null, null, BufferedImage.class); setTMAEntriesFromImageData(imageData); return; } List<TMAEntry> entriesTemp = new ArrayList<>(); File dir = file.isDirectory() ? file : file.getParentFile(); for (File fileInput : dir.listFiles()) { if (fileInput.isHidden() || fileInput.isDirectory() || !fileInput.getName().toLowerCase().endsWith(".qptma")) continue; parseInputFile(fileInput, entriesTemp); } if (entriesTemp.isEmpty()) { logger.error("No data found for " + file.getAbsolutePath()); return; } setTMAEntries(entriesTemp); stage.setTitle("TMA Results View: " + dir.getName()); } void setTMAEntries(final Collection<TMAEntry> newEntries) { // boolean containsSummaries = newEntries.stream().anyMatch(e -> e instanceof TMASummaryEntry); // Turn off use-selected - can be crashy when replacing entries if (!newEntries.equals(entriesBase)) { useSelectedProperty.set(false); // Reset the cache imageCache.clear(); // Try to load small images in a background thread List<TMAEntry> duplicateEntries = new ArrayList<>(newEntries); ExecutorService service = Executors.newSingleThreadExecutor(); service.submit(() -> { duplicateEntries.parallelStream().forEach(entry -> { imageCache.getImage(entry, maxSmallWidth.get()); imageCache.getOverlay(entry, maxSmallWidth.get()); }); }); service.shutdown(); } this.entriesBase.setAll(newEntries); // Store the names of any currently hidden columns lastHiddenColumns = table.getColumns().stream().filter(c -> !c.isVisible()).map(c -> c.getText()) .collect(Collectors.toSet()); // this.table.getColumns().clear(); // // Useful for a paper, but not generally... // int count = 0; // int nCells = 0; // int nTumor = 0; // for (TMAEntry entry : entriesBase) { // if (!entry.isMissing() && (predicate.get() == null || predicate.get().test(entry))) { // count++; // nCells += (int)(entry.getMeasurement("Num Tumor").doubleValue() + entry.getMeasurement("Num Stroma").doubleValue()); // nTumor += (int)(entry.getMeasurement("Num Tumor").doubleValue()); // } // } // System.err.println(String.format("Num entries:\t%d\tNum tumor:\t%d\tNum cells:\t%d", count, nTumor, nCells)); // Update measurement names Set<String> namesMeasurements = new LinkedHashSet<>(); Set<String> namesMetadata = new LinkedHashSet<>(); // boolean containsSummaries = false; for (TMAEntry entry : newEntries) { namesMeasurements.addAll(entry.getMeasurementNames()); namesMetadata.addAll(entry.getMetadataNames()); // containsSummaries = containsSummaries || entry instanceof TMASummaryEntry; } // Get the available survival columns String currentSurvival = getSurvivalColumn(); survivalColumns.clear(); if (namesMeasurements.contains(TMACoreObject.KEY_OVERALL_SURVIVAL)) survivalColumns.add(TMACoreObject.KEY_OVERALL_SURVIVAL); if (namesMeasurements.contains(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL)) survivalColumns.add(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL); if (currentSurvival != null && survivalColumns.contains(currentSurvival)) comboSurvival.getSelectionModel().select(currentSurvival); else if (!survivalColumns.isEmpty()) comboSurvival.getSelectionModel().select(survivalColumns.get(0)); // // Add the count of non-missing cores if we are working with summaries // if (containsSummaries) namesMeasurements.add("Available cores"); // Make sure there are no nulls or other unusable values namesMeasurements.remove(null); namesMeasurements.remove(""); // measurementNames.clear(); String selectedMainMeasurement = comboMainMeasurement.getSelectionModel().getSelectedItem(); measurementNames.setAll(namesMeasurements); if (namesMeasurements.contains(selectedMainMeasurement)) comboMainMeasurement.getSelectionModel().select(selectedMainMeasurement); else { namesMeasurements.remove(TMACoreObject.KEY_UNIQUE_ID); namesMeasurements.remove(TMACoreObject.KEY_OVERALL_SURVIVAL); namesMeasurements.remove(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL); namesMeasurements.remove(TMACoreObject.KEY_OS_CENSORED); namesMeasurements.remove(TMACoreObject.KEY_RFS_CENSORED); namesMeasurements.remove("Censored"); // For historical reasons when there was only one censored column supported... if (!namesMeasurements.isEmpty()) comboMainMeasurement.getSelectionModel().select(0); } metadataNames.clear(); metadataNames.addAll(namesMetadata); refreshTableData(); // The next time the table is empty, show a different placeholder // from the original (which is for loading/import) table.setPlaceholder(new Text("No data")); } private void refreshTableData() { // int nn = 0; // double nPositive = 0; // for (TMAEntry entry : entriesBase) { // if (entry.isMissing()) // continue; // nPositive += entry.getMeasurementAsDouble("Num Positive"); // nn++; // } // System.err.println(nPositive + " positive cells across " + nn + " tissue samples"); Collection<? extends TMAEntry> entries = groupByIDProperty.get() ? createSummaryEntries(entriesBase) : entriesBase; // Ensure that we don't try to modify a filtered list List<TreeTableColumn<TMAEntry, ?>> columns = new ArrayList<>(); // Add an empty column. // Its purpose is to provide the space needed for the little expansion arrows, to avoid // these stealing space from the first interesting column. // Note: there's nothing to prevent the user reordering it along with other columns... // but hopefully it looks 'right' enough where it is that few would try to do that TreeTableColumn<TMAEntry, String> columnEmpty = new TreeTableColumn<>(" "); columnEmpty .setCellValueFactory(new Callback<CellDataFeatures<TMAEntry, String>, ObservableValue<String>>() { @Override public ObservableValue<String> call(CellDataFeatures<TMAEntry, String> p) { return Bindings.createStringBinding(() -> ""); } }); columnEmpty.setSortable(false); columnEmpty.setResizable(false); columns.add(columnEmpty); // Check if we have any images or overlays boolean hasImages = entries.stream().anyMatch(e -> e.hasImage()); boolean hasOverlay = entries.stream().anyMatch(e -> e.hasOverlay()); // Add columns to show images, if we have them if (hasImages || hasOverlay) { TreeTableColumn<TMAEntry, TMAEntry> columnImage = hasImages ? new TreeTableColumn<>("Thumbnail") : null; TreeTableColumn<TMAEntry, TMAEntry> columnOverlay = hasOverlay ? new TreeTableColumn<>("Overlay") : null; if (hasImages) { columnImage.setCellValueFactory( new Callback<CellDataFeatures<TMAEntry, TMAEntry>, ObservableValue<TMAEntry>>() { @Override public ObservableValue<TMAEntry> call(CellDataFeatures<TMAEntry, TMAEntry> p) { return p.getValue().valueProperty(); } }); columnImage.setCellFactory(c -> new ImageTableCell(imageCache, false)); columnImage.maxWidthProperty().bind(maxSmallWidth); columnImage.widthProperty().addListener((v, o, n) -> { if (n.doubleValue() == columnImage.getPrefWidth()) return; if (hasOverlay) columnOverlay.setPrefWidth(n.doubleValue()); table.refresh(); }); columns.add(columnImage); } if (hasOverlay) { columnOverlay.setCellValueFactory( new Callback<CellDataFeatures<TMAEntry, TMAEntry>, ObservableValue<TMAEntry>>() { @Override public ObservableValue<TMAEntry> call(CellDataFeatures<TMAEntry, TMAEntry> p) { return p.getValue().valueProperty(); } }); columnOverlay.setCellFactory(c -> new ImageTableCell(imageCache, true)); columnOverlay.maxWidthProperty().bind(maxSmallWidth); columnOverlay.widthProperty().addListener((v, o, n) -> { if (n.doubleValue() == columnOverlay.getPrefWidth()) return; columnImage.setPrefWidth(n.doubleValue()); if (hasImages) table.refresh(); }); columns.add(columnOverlay); } } // Update image availability if (hasImages) { if (hasOverlay) imageAvailability.set(ImageAvailability.BOTH); else imageAvailability.set(ImageAvailability.IMAGE_ONLY); } else if (hasOverlay) { imageAvailability.set(ImageAvailability.OVERLAY_ONLY); } else imageAvailability.set(ImageAvailability.NONE); for (String name : model.getAllNames()) { if (model.getMeasurementNames().contains(name)) { TreeTableColumn<TMAEntry, Number> column = new TreeTableColumn<>(name); column.setCellValueFactory( new Callback<CellDataFeatures<TMAEntry, Number>, ObservableValue<Number>>() { @Override public ObservableValue<Number> call(CellDataFeatures<TMAEntry, Number> p) { double value = p.getValue() == null ? Double.NaN : model.getNumericValue(p.getValue().getValue(), name); return new SimpleDoubleProperty(value); } }); column.setCellFactory(c -> new NumericTableCell<>()); columns.add(column); } else { TreeTableColumn<TMAEntry, Object> column = new TreeTableColumn<>(name); column.setCellValueFactory( new Callback<CellDataFeatures<TMAEntry, Object>, ObservableValue<Object>>() { @Override public ObservableValue<Object> call(CellDataFeatures<TMAEntry, Object> p) { return new SimpleObjectProperty<>(p.getValue() == null ? null : model.getStringValue(p.getValue().getValue(), name)); } }); column.setCellFactory(c -> new BasicTableCell<>()); columns.add(column); } } // Set the column visibility depending upon whether they were hidden previously columns.stream().forEach(c -> c.setVisible(!lastHiddenColumns.contains(c.getText()))); // Set columns for table table.getColumns().setAll(columns); // Set new root for table TreeItem<TMAEntry> root = new RootTreeItem(entries, combinedPredicate); table.setShowRoot(false); table.setRoot(root); model.refreshList(); } static class RootTreeItem extends TreeItem<TMAEntry> implements ChangeListener<Predicate<TMAEntry>> { private List<TreeItem<TMAEntry>> entries = new ArrayList<>(); private ObservableValue<Predicate<TMAEntry>> combinedPredicate; RootTreeItem(final Collection<? extends TMAEntry> entries, final ObservableValue<Predicate<TMAEntry>> combinedPredicate) { super(null); for (TMAEntry entry : entries) { if (entry instanceof TMASummaryEntry) this.entries.add(new SummaryTreeItem((TMASummaryEntry) entry)); else this.entries.add(new TreeItem<>(entry)); } this.combinedPredicate = combinedPredicate; this.combinedPredicate.addListener(new WeakChangeListener<>(this)); updateChildren(); } private void updateChildren() { ArrayList<TreeItem<TMAEntry>> children = new ArrayList<>(); for (TreeItem<TMAEntry> entry : entries) { if (entry instanceof SummaryTreeItem) { SummaryTreeItem summaryItem = (SummaryTreeItem) entry; summaryItem.updateChildren(); if (!summaryItem.getChildren().isEmpty()) children.add(summaryItem); } else if (combinedPredicate.getValue().test(entry.getValue())) children.add(entry); } super.getChildren().setAll(children); } @Override public void changed(ObservableValue<? extends Predicate<TMAEntry>> observable, Predicate<TMAEntry> oldValue, Predicate<TMAEntry> newValue) { updateChildren(); } } static class SummaryTreeItem extends TreeItem<TMAEntry> { private TMASummaryEntry entry; SummaryTreeItem(final TMASummaryEntry entry) { super(entry); this.entry = entry; updateChildren(); } private void updateChildren() { ArrayList<TreeItem<TMAEntry>> children = new ArrayList<>(); for (TMAEntry subEntry : entry.getEntries()) children.add(new TreeItem<>(subEntry)); super.getChildren().setAll(children); } } // class SummaryTreeItem extends TreeItem<TMAEntry> implements ChangeListener<Predicate<? super TMAEntry>> { // // private TMASummaryEntry entry; // // SummaryTreeItem(final TMASummaryEntry entry) { // super(entry); // this.entry = entry; // combinedPredicate.addListener(new WeakChangeListener<Predicate<? super TMAEntry>>(this)); // updateChildren(); // } // // private void updateChildren() { // ArrayList<TreeItem<TMAEntry>> children = new ArrayList<>(); // for (TMAEntry subEntry : entry.getEntries()) // children.add(new TreeItem<>(subEntry)); // super.getChildren().setAll(children); // } // // @Override // public void changed(ObservableValue<? extends Predicate<? super TMAEntry>> observable, // Predicate<? super TMAEntry> oldValue, Predicate<? super TMAEntry> newValue) { // updateChildren(); // } // // } /** * Create summaries entries by grouping according to Unique ID. * * @param entries * @return */ private Collection<? extends TMAEntry> createSummaryEntries(final List<? extends TMAEntry> entries) { Map<String, TMASummaryEntry> summaryEntryMap = new TreeMap<>(); int maxSummaryLength = 0; for (TMAEntry entry : entries) { String id = entry.getMetadataValue(TMACoreObject.KEY_UNIQUE_ID); if (id == null && entry.getMeasurement(TMACoreObject.KEY_UNIQUE_ID) != null) id = entry.getMeasurement(TMACoreObject.KEY_UNIQUE_ID).toString(); if (id == null || id.trim().length() == 0) { if (!"True".equals(entry.getMetadataValue(MISSING_COLUMN))) logger.trace("No ID found for {}", entry); continue; } TMASummaryEntry summary = summaryEntryMap.get(id); if (summary == null) { summary = new TMASummaryEntry(selectedMeasurementCombinationProperty, skipMissingCoresProperty, combinedPredicate); summaryEntryMap.put(id, summary); } summary.addEntry(entry); maxSummaryLength = Math.max(maxSummaryLength, summary.getEntries().size()); } // If we don't have any summaries, just return the original entries if (summaryEntryMap.isEmpty() || maxSummaryLength <= 1) return entries; return summaryEntryMap.values(); } private void parseInputFile(File file, List<TMAEntry> entries) { int nEntries = entries.size(); String serverPath = null; try { Scanner scanner = new Scanner(file); serverPath = scanner.nextLine().trim(); scanner.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } if (serverPath == null) { // || !(new File(serverPath).exists())) { logger.error("Unable to find a server with path " + serverPath + " - cannot parse " + file.getAbsolutePath()); return; } File dirData = new File(file.getAbsolutePath() + ".data"); try { Map<String, List<String>> csvData = TMAScoreImporter.readCSV(getTMAResultsFile(dirData)); if (csvData.isEmpty()) return; // Identify metadata and numeric columns Map<String, List<String>> metadataColumns = new LinkedHashMap<>(); Map<String, double[]> measurementColumns = new LinkedHashMap<>(); List<String> idColumn = csvData.remove(TMACoreObject.KEY_UNIQUE_ID); if (idColumn != null) { metadataColumns.put(TMACoreObject.KEY_UNIQUE_ID, idColumn); // Make sure IDs are trimmed if (trimUniqueIDs) { for (int i = 0; i < idColumn.size(); i++) idColumn.set(i, idColumn.get(i).trim()); } } List<String> nameColumn = csvData.remove("Name"); if (nameColumn == null) nameColumn = csvData.remove("Object"); // Handle 'missing-ness' separately from general metadata List<String> missingColumn = csvData.remove(MISSING_COLUMN); int n = idColumn == null ? 0 : idColumn.size(); //csvData.values().iterator().next().size(); for (Entry<String, List<String>> entry : csvData.entrySet()) { List<String> list = entry.getValue(); n = list.size(); double[] values = TMAScoreImporter.parseNumeric(list, true); if (values == null || TMAScoreImporter.numNaNs(values) == list.size()) metadataColumns.put(entry.getKey(), list); else measurementColumns.put(entry.getKey(), values); } for (int i = 0; i < n; i++) { // Don't permit 'NaN' as an ID if (idColumn != null && "NaN".equals(idColumn.get(i))) continue; String name = nameColumn == null ? idColumn.get(i) : nameColumn.get(i); boolean missing = missingColumn != null && "True".equals(missingColumn.get(i)); File fileImage = new File(dirData, name + ".jpg"); File fileOverlayImage = new File(dirData, name + "-overlay.jpg"); if (!fileOverlayImage.exists()) fileOverlayImage = new File(dirData, name + "-overlay.png"); TMAEntry entry = new DefaultTMAEntry(serverPath, fileImage.getAbsolutePath(), fileOverlayImage.getAbsolutePath(), name, missing); for (Entry<String, List<String>> temp : metadataColumns.entrySet()) { entry.putMetadata(temp.getKey(), temp.getValue().get(i)); } for (Entry<String, double[]> temp : measurementColumns.entrySet()) { entry.putMeasurement(temp.getKey(), temp.getValue()[i]); } entries.add(entry); } } catch (Exception e) { e.printStackTrace(); } logger.info("Parsed " + (entries.size() - nEntries) + " from " + file.getName() + " (" + entries.size() + " total)"); } private File getTMAResultsFile(File dir) { for (File file : dir.listFiles()) if ((file.getName().startsWith("TMA results") || file.getName().startsWith("TMA_results")) && file.getName().endsWith(".txt")) return file; return null; } private class TMATableModel implements PathTableData<TMAEntry> { private ObservableList<TMAEntry> list = FXCollections.observableArrayList(); TMATableModel() { useSelectedProperty.addListener((v, o, n) -> refreshList()); table.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<TreeItem<TMAEntry>>() { @Override public void onChanged(ListChangeListener.Change<? extends TreeItem<TMAEntry>> c) { if (useSelectedProperty.get()) refreshList(); } }); refreshList(); } private void refreshList() { if (table.getRoot() == null) list.clear(); else if (useSelectedProperty.get()) { List<TMAEntry> selectedList = table.getSelectionModel().getSelectedItems().stream() .map(i -> i.getValue()).collect(Collectors.toList()); // If we have *any* summary entries, then make sure we have *all* summary entries if (selectedList.stream().anyMatch(e -> e instanceof TMASummaryEntry)) selectedList = selectedList.stream().filter(e -> e instanceof TMASummaryEntry) .collect(Collectors.toList()); list.setAll(selectedList); } else list.setAll( table.getRoot().getChildren().stream().map(i -> i.getValue()).collect(Collectors.toList())); } @Override public List<String> getAllNames() { List<String> namesList = new ArrayList<>(); namesList.add("Image"); namesList.add("Core"); namesList.addAll(metadataNames); namesList.addAll(measurementNames); namesList.add("Comment"); return namesList; } @Override public String getStringValue(TMAEntry entry, String column) { return getStringValue(entry, column, -1); } @Override public String getStringValue(TMAEntry entry, String column, int decimalPlaces) { if ("Image".equals(column)) return entry.getImageName(); if ("Core".equals(column)) return entry.getName(); if ("Comment".equals(column)) return entry.getComment(); // if ("Non missing".equals(column)) // return entry instanceof TMASummaryEntry ? Integer.toString(((TMASummaryEntry)entry).nNonMissingEntries()) : ""; if (metadataNames.contains(column)) return entry.getMetadataValue(column); double val = getNumericValue(entry, column); if (Double.isNaN(val)) return "NaN"; return GeneralTools.formatNumber(getNumericValue(entry, column), 4); } @Override public List<String> getMeasurementNames() { return measurementNames; } @Override public double getNumericValue(TMAEntry entry, String column) { if (entry == null) return Double.NaN; if ("Available cores".equals(column)) return entry instanceof TMASummaryEntry ? ((TMASummaryEntry) entry).nNonMissingEntries() : Double.NaN; Number value = entry.getMeasurement(column); return value == null ? Double.NaN : value.doubleValue(); } @Override public double[] getDoubleValues(String column) { List<TMAEntry> entries = getEntries(); double[] values = new double[entries.size()]; for (int i = 0; i < entries.size(); i++) values[i] = getNumericValue(entries.get(i), column); return values; } @Override public ObservableList<TMAEntry> getEntries() { return list; // if (useSelectedProperty.get()) // return Collections.unmodifiableList(table.getSelectionModel().getSelectedItems()); // return Collections.unmodifiableList(table.getItems()); } } private void promptForComment() { String input = DisplayHelpers.showInputDialog("Add comment", "Type comment for " + entrySelected.getName() + "(" + entrySelected.getImageName() + ")", entrySelected.getComment()); if (input == null) return; entrySelected.setComment(input); table.refresh(); } class ScatterPane { private BorderPane pane = new BorderPane(); private ComboBox<String> comboScatterMainMeasurement = new ComboBox<>(); private ComboBox<String> comboScatterSecondaryMeasurement = new ComboBox<>(); private NumberAxis xAxis = new NumberAxis(); private NumberAxis yAxis = new NumberAxis(); private ScatterChart<Number, Number> chart = new ScatterChart<>(xAxis, yAxis); private TableView<DoubleProperty> tableScatter = new TableView<>(); ScatterPane() { comboScatterMainMeasurement.setItems(measurementNames); comboScatterSecondaryMeasurement.setItems(measurementNames); comboMainMeasurement.getSelectionModel().selectedItemProperty() .addListener((v, o, n) -> comboScatterMainMeasurement.getSelectionModel().select(n)); comboScatterMainMeasurement.getSelectionModel().selectedItemProperty() .addListener((v, o, n) -> comboMainMeasurement.getSelectionModel().select(n)); comboScatterMainMeasurement.getSelectionModel().selectedItemProperty() .addListener((v, o, n) -> updateChart()); comboScatterSecondaryMeasurement.getSelectionModel().selectedItemProperty() .addListener((v, o, n) -> updateChart()); GridPane topGrid = new GridPane(); Label label = new Label("Main measurement"); label.minWidthProperty().bind(label.prefWidthProperty()); topGrid.add(label, 0, 0); topGrid.add(comboScatterMainMeasurement, 1, 0); label = new Label("Secondary measurement"); label.minWidthProperty().bind(label.prefWidthProperty()); topGrid.add(label, 0, 1); topGrid.add(comboScatterSecondaryMeasurement, 1, 1); topGrid.setHgap(5); comboScatterMainMeasurement.setMaxWidth(Double.MAX_VALUE); comboScatterSecondaryMeasurement.setMaxWidth(Double.MAX_VALUE); GridPane.setHgrow(comboScatterMainMeasurement, Priority.ALWAYS); GridPane.setHgrow(comboScatterSecondaryMeasurement, Priority.ALWAYS); topGrid.setPadding(new Insets(5, 10, 5, 10)); topGrid.prefWidthProperty().bind(pane.widthProperty()); // Set up table TableColumn<DoubleProperty, String> colName = new TableColumn<>("Name"); colName.setCellValueFactory(v -> new SimpleStringProperty(v.getValue().getName())); TableColumn<DoubleProperty, String> colValue = new TableColumn<>("Value"); colValue.setCellValueFactory( v -> new SimpleStringProperty(GeneralTools.formatNumber(v.getValue().getValue(), 3))); tableScatter.getColumns().add(colName); tableScatter.getColumns().add(colValue); tableScatter.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableScatter.setPrefHeight(25 * 8); pane.setTop(topGrid); pane.setCenter(chart); pane.setBottom(tableScatter); // Make it possible to navigate around the chart ChartToolsFX.makeChartInteractive(chart, xAxis, yAxis); ChartToolsFX.addChartExportMenu(chart, null); } public Pane getPane() { return pane; } private void updateChart() { String xMeasurement = comboScatterMainMeasurement.getSelectionModel().getSelectedItem(); String yMeasurement = comboScatterSecondaryMeasurement.getSelectionModel().getSelectedItem(); double[] x = model.getDoubleValues(xMeasurement); double[] y = model.getDoubleValues(yMeasurement); int count = 0; List<TMAEntry> entries = model.getEntries(); ObservableList<XYChart.Data<Number, Number>> data = FXCollections.observableArrayList(); for (int i = 0; i < x.length; i++) { double xx = x[i]; double yy = y[i]; if (Double.isNaN(xx) || Double.isNaN(yy)) continue; // Adding jitter (but need to consider axis scaling) // xx = xx + Math.random()/5; // yy = yy + Math.random()/5; XYChart.Data<Number, Number> item = new XYChart.Data<>(xx, yy, entries.get(i)); data.add(item); // Shift values back to replace any NaNs x[count] = xx; y[count] = yy; count++; } if (chart.getData().isEmpty()) chart.getData().add(new XYChart.Series<>(data)); else chart.getData().get(0).setData(data); for (XYChart.Data<Number, Number> element : data) { Node node = element.getNode(); Object value = element.getExtraValue(); if (value instanceof TMAEntry) { TMAEntry entry = (TMAEntry) value; if (entry.getMeasurement(colCensored) != null && entry.getMeasurement(colCensored).doubleValue() == 1) node.setStyle( "" + "-fx-background-color: rgb(60, 200, 60, 0.75); " + "-fx-opacity: 0.5;" + ""); else { node.setStyle("" + "-fx-opacity: 0.75;" + ""); } node.setOnMouseClicked(e -> { // Only clear selection if selection isn't used for display if (!useSelectedProperty.get()) table.getSelectionModel().clearSelection(); // Select the item TreeItem<TMAEntry> item = getItem(table.getRoot(), entry); if (item != null) { item.setExpanded(true); // if (item.getParent() != null) // item.getParent().setExpanded(true); table.getSelectionModel().select(item); table.layout(); int ind = table.getSelectionModel().getSelectedIndex(); if (ind >= 0) { table.scrollTo(ind); } } }); } DropShadow dropShadow = new DropShadow(); Node nodeFinal = node; nodeFinal.hoverProperty().addListener((v, o, n) -> { nodeFinal.setEffect(n ? dropShadow : null); }); } xAxis.setLabel(xMeasurement); yAxis.setLabel(yMeasurement); chart.setLegendVisible(false); // Try to update table data if (count == 0) { tableScatter.getItems().clear(); return; } int len = x.length; int nNanX = TMAScoreImporter.numNaNs(x); int nNanY = TMAScoreImporter.numNaNs(y); if (count < x.length) { x = Arrays.copyOf(x, count); y = Arrays.copyOf(y, count); } tableScatter.getItems().setAll( new SimpleDoubleProperty(null, "Total '" + xMeasurement + "'", len - nNanX), new SimpleDoubleProperty(null, "Total '" + yMeasurement + "'", len - nNanY), new SimpleDoubleProperty(null, String.format("Total '%s' & '%s'", xMeasurement, yMeasurement), count)); if (count > 1) { double pearsons = new PearsonsCorrelation().correlation(x, y); double spearmans = new SpearmansCorrelation().correlation(x, y); tableScatter.getItems().addAll( new SimpleDoubleProperty(null, "Pearson's correlation coefficient", pearsons), new SimpleDoubleProperty(null, "Spearman's correlation coefficient", spearmans)); } } } /** * Recursively search for a TreeItem, based upon the TMAEntry it represents. * * @param item * @param entry * @return */ private TreeItem<TMAEntry> getItem(final TreeItem<TMAEntry> item, final TMAEntry entry) { if (item == null) return null; if (item.getValue() == entry) return item; for (TreeItem<TMAEntry> item2 : item.getChildren()) { TreeItem<TMAEntry> found = getItem(item2, entry); if (found != null) return found; } return null; } private int importScores(final String text) { Map<String, List<String>> data = TMAScoreImporter.readCSV(text); List<String> idColumn = data.remove(TMACoreObject.KEY_UNIQUE_ID); if (idColumn == null) { DisplayHelpers.showErrorMessage("Import TMA data", "No '" + TMACoreObject.KEY_UNIQUE_ID + "' column found!"); return 0; } // Nothing left to import... if (data.isEmpty()) return 0; // Get the numeric columns, if possible Map<String, double[]> dataNumeric = new HashMap<>(); for (String key : data.keySet().toArray(new String[0])) { double[] vals = TMAScoreImporter.parseNumeric(data.get(key), true); if (vals != null && TMAScoreImporter.numNaNs(vals) != vals.length) { dataNumeric.put(key, vals); data.remove(key); } } // Loop through IDs, adding values where needed int counter = 0; for (int i = 0; i < idColumn.size(); i++) { boolean matched = false; String id = idColumn.get(i); if (id == null) { logger.debug("Skipping missing ID"); continue; } for (TMAEntry entry : entriesBase) { if (id.equals(entry.getMetadataValue(TMACoreObject.KEY_UNIQUE_ID))) { matched = true; for (Entry<String, double[]> dataEntry : dataNumeric.entrySet()) { entry.putMeasurement(dataEntry.getKey(), dataEntry.getValue()[i]); } for (Entry<String, List<String>> dataEntry : data.entrySet()) { entry.putMetadata(dataEntry.getKey(), dataEntry.getValue().get(i)); } counter++; } } if (!matched) logger.warn("No match for ID: " + id); } Optional<TMAEntry> objectEntry = entriesBase.stream().filter(t -> t instanceof TMAObjectEntry).findAny(); if (objectEntry.isPresent()) { DisplayHelpers.showInfoNotification("TMA data update", "TMA cores updated!"); } return counter; } /** * This is admittedly not the most beautiful or safe way to deal with an arbitrary predicate, * but a bit of sanity-checking & cleanup hopefully avoids the security risk of running a full Javascript * engine as a glorified expression parser. * * Its use is to filter out particular TMAEntries, so they don't contribute to any summaries. */ static class TablePredicate implements Predicate<TMAEntry> { final String commandOriginal; final String command; final SimpleBindings bindings = new SimpleBindings(); final ScriptEngine engine; private boolean lastEvaluationSucceeded = true; private boolean isValid = false; TablePredicate(final String predicate) { // Predicates are only allowed to contain quoted strings (converted to measurement value requests) // as well as operators or parentheses this.commandOriginal = predicate; String quotedRegex = "\"([^\"]*)\""; String test = predicate.replaceAll(quotedRegex, ""); isValid = test.replaceAll("[ ()+-<>=*/&|!]", "").trim().isEmpty(); // Check we don't have invalid characters if (isValid) { this.command = predicate.replaceAll(quotedRegex, "entry.getMeasurementAsDouble(\"$1\")").trim(); } else this.command = null; ScriptEngineManager manager = new ScriptEngineManager(); engine = manager.getEngineByName("JavaScript"); engine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE); } @Override public boolean test(TMAEntry entry) { if (!isValid) throw new RuntimeException("Cannot run invalid predicate! Original command: " + commandOriginal); // If nothing is included, accept everything if (this.command.isEmpty()) return true; // Run script to deal with predicate if required bindings.put("entry", entry); try { Object result = engine.eval(command); lastEvaluationSucceeded = result instanceof Boolean; return Boolean.TRUE.equals(result); } catch (ScriptException e) { lastEvaluationSucceeded = false; logger.error("Error evaluating {} for {}: {}", command, entry, e.getLocalizedMessage()); return false; } } public String getOriginalCommand() { return commandOriginal; } public String getCommand() { return command; } public boolean isValid() { return isValid; } public boolean lastEvaluationSucceeded() { return lastEvaluationSucceeded; } @Override public String toString() { return getCommand(); } } }