Java tutorial
/* * Copyright (C) 2017 Gaurav Vaidya <gaurav@ggvaidya.com> * * 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/>. */ package com.ggvaidya.scinames.dataset; import java.awt.Desktop; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import com.ggvaidya.scinames.model.Change; import com.ggvaidya.scinames.model.ChangeType; import com.ggvaidya.scinames.model.Dataset; import com.ggvaidya.scinames.model.DatasetColumn; import com.ggvaidya.scinames.model.DatasetRow; import com.ggvaidya.scinames.model.Name; import com.ggvaidya.scinames.model.Project; import com.ggvaidya.scinames.model.change.ChangeTypeStringConverter; import com.ggvaidya.scinames.model.change.NameSetStringConverter; import com.ggvaidya.scinames.model.filters.ChangeFilter; import com.ggvaidya.scinames.tabulardata.TabularDataViewController; import javafx.beans.Observable; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.collections.ObservableSet; import javafx.collections.transformation.SortedList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Dialog; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.SelectionMode; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.cell.ComboBoxTableCell; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.TextFieldTableCell; import javafx.stage.FileChooser; import javafx.stage.Stage; /** * FXML Controller class for a view of a Dataset in a project. This does a bunch of cool * things: * * - 1. We provide editable information on dataset rows for a dataset. * - 2. We provide editable information on changes for a checklist. * * @author Gaurav Vaidya <gaurav@ggvaidya.com> */ public class DatasetSceneController { private static final Logger LOGGER = Logger.getLogger(DatasetSceneController.class.getSimpleName()); /** * If a dataset contains more than this number of changes, then we won't calculate additional * data on them at all. (Eventually, we should just calculate additional data */ public static final int ADDITIONAL_DATA_CHANGE_COUNT_LIMIT = 150; private DatasetChangesView datasetView; private Dataset dataset; public DatasetSceneController() { } public void setTimepointView(DatasetChangesView tv) { datasetView = tv; dataset = tv.getDataset(); // Reinitialize UI to the selected timepoint. setupTableWithChanges(changesTableView, dataset); dataset.lastModifiedProperty().addListener(cl -> { fillTableWithChanges(changesTableView, dataset); }); updateAdditionalData(); LOGGER.info("Finished setTimepointView()"); } /** * Initializes the controller class. */ public void initialize() { initAdditionalData(); setupMagicButtons(); } /* * User interface. */ @FXML private TableView<Change> changesTableView; private void setupTableWithChanges(TableView<Change> tv, Dataset tp) { tv.setEditable(true); tv.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); tv.getColumns().clear(); TableColumn<Change, ChangeType> colChangeType = new TableColumn<>("Type"); colChangeType.setCellFactory(ComboBoxTableCell.forTableColumn(new ChangeTypeStringConverter(), ChangeType.ADDITION, ChangeType.DELETION, ChangeType.RENAME, ChangeType.LUMP, ChangeType.SPLIT, ChangeType.COMPLEX, ChangeType.ERROR)); colChangeType.setCellValueFactory(new PropertyValueFactory<>("type")); colChangeType.setPrefWidth(100.0); colChangeType.setEditable(true); tv.getColumns().add(colChangeType); TableColumn<Change, ObservableSet<Name>> colChangeFrom = new TableColumn<>("From"); colChangeFrom.setCellFactory(TextFieldTableCell.forTableColumn(new NameSetStringConverter())); colChangeFrom.setCellValueFactory(new PropertyValueFactory<>("from")); colChangeFrom.setPrefWidth(200.0); colChangeFrom.setEditable(true); tv.getColumns().add(colChangeFrom); TableColumn<Change, ObservableSet<Name>> colChangeTo = new TableColumn<>("To"); colChangeTo.setCellFactory(TextFieldTableCell.forTableColumn(new NameSetStringConverter())); colChangeTo.setCellValueFactory(new PropertyValueFactory<>("to")); colChangeTo.setPrefWidth(200.0); colChangeTo.setEditable(true); tv.getColumns().add(colChangeTo); TableColumn<Change, String> colExplicit = new TableColumn<>("Explicit or implicit?"); colExplicit.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( features.getValue().getDataset().isChangeImplicit(features.getValue()) ? "Implicit" : "Explicit")); tv.getColumns().add(colExplicit); ChangeFilter cf = datasetView.getProjectView().getProject().getChangeFilter(); TableColumn<Change, String> colFiltered = new TableColumn<>("Eliminated by filter?"); colFiltered.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( cf.test(features.getValue()) ? "Allowed" : "Eliminated")); tv.getColumns().add(colFiltered); TableColumn<Change, String> colNote = new TableColumn<>("Note"); colNote.setCellFactory(TextFieldTableCell.forTableColumn()); colNote.setCellValueFactory(new PropertyValueFactory<>("note")); colNote.setPrefWidth(100.0); colNote.setEditable(true); tv.getColumns().add(colNote); TableColumn<Change, String> colCitations = new TableColumn<>("Citations"); colCitations.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( features.getValue().getCitationStream().map(citation -> citation.getCitation()).sorted() .collect(Collectors.joining("; ")))); tv.getColumns().add(colCitations); TableColumn<Change, String> colGenera = new TableColumn<>("Genera"); colGenera.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( String.join(", ", features.getValue().getAllNames().stream().map(n -> n.getGenus()) .distinct().sorted().collect(Collectors.toList())))); tv.getColumns().add(colGenera); TableColumn<Change, String> colSpecificEpithet = new TableColumn<>("Specific epithets"); colSpecificEpithet.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper(String .join(", ", features.getValue().getAllNames().stream().map(n -> n.getSpecificEpithet()) .filter(s -> s != null).distinct().sorted().collect(Collectors.toList())))); tv.getColumns().add(colSpecificEpithet); // The infraspecific string. TableColumn<Change, String> colInfraspecificEpithet = new TableColumn<>("Infraspecific epithets"); colInfraspecificEpithet.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( String.join(", ", features.getValue().getAllNames().stream() .map(n -> n.getInfraspecificEpithetsAsString()).filter(s -> s != null) .distinct().sorted().collect(Collectors.toList())))); tv.getColumns().add(colInfraspecificEpithet); // The very last epithet of all TableColumn<Change, String> colTerminalEpithet = new TableColumn<>("Terminal epithet"); colTerminalEpithet.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( String.join(", ", features.getValue().getAllNames().stream().map(n -> { List<Name.InfraspecificEpithet> infraspecificEpithets = n.getInfraspecificEpithets(); if (!infraspecificEpithets.isEmpty()) { return infraspecificEpithets.get(infraspecificEpithets.size() - 1).getValue(); } else { return n.getSpecificEpithet(); } }).filter(s -> s != null).distinct().sorted().collect(Collectors.toList())))); tv.getColumns().add(colTerminalEpithet); // Properties TableColumn<Change, String> colProperties = new TableColumn<>("Properties"); colProperties.setCellValueFactory( (TableColumn.CellDataFeatures<Change, String> features) -> new ReadOnlyStringWrapper( features.getValue().getProperties().entrySet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()).sorted() .collect(Collectors.joining("; ")))); tv.getColumns().add(colProperties); fillTableWithChanges(tv, tp); // When someone selects a cell in the Table, try to select the appropriate data in the // additional data view. tv.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Change>) lcl -> { AdditionalData aData = additionalDataCombobox.getSelectionModel().getSelectedItem(); if (aData != null) { aData.onSelectChange(tv.getSelectionModel().getSelectedItems()); } }); // Create a right-click menu for table rows. changesTableView.setRowFactory(table -> { TableRow<Change> row = new TableRow<>(); row.setOnContextMenuRequested(event -> { if (row.isEmpty()) return; // We don't currently use the clicked change, since currently all options // change *all* the selected changes, but this may change in the future. Change change = row.getItem(); ContextMenu changeMenu = new ContextMenu(); Menu searchForName = new Menu("Search for name"); searchForName.getItems().addAll( change.getAllNames().stream().sorted().map(n -> createMenuItem(n.getFullName(), action -> { datasetView.getProjectView().openDetailedView(n); })).collect(Collectors.toList())); changeMenu.getItems().add(searchForName); changeMenu.getItems().add(new SeparatorMenuItem()); changeMenu.getItems().add(createMenuItem("Edit note", action -> { List<Change> changes = new ArrayList<>(changesTableView.getSelectionModel().getSelectedItems()); String combinedNotes = changes.stream().map(ch -> ch.getNote().orElse("").trim()).distinct() .collect(Collectors.joining("\n")).trim(); Optional<String> result = askUserForTextArea( "Modify the note for these " + changes.size() + " changes:", combinedNotes); if (result.isPresent()) { String note = result.get().trim(); LOGGER.info("Using 'Edit note' to set note to '" + note + "' on changes " + changes); changes.forEach(ch -> ch.noteProperty().set(note)); } })); changeMenu.getItems().add(new SeparatorMenuItem()); // Create a submenu for tags and urls. String note = change.noteProperty().get(); Menu removeTags = new Menu("Tags"); removeTags.getItems().addAll(change.getTags().stream().sorted() .map(tag -> new MenuItem(tag.getName())).collect(Collectors.toList())); Menu lookupURLs = new Menu("Lookup URL"); change.getURIs().stream().sorted().map(uri -> { return createMenuItem(uri.toString(), evt -> { try { Desktop.getDesktop().browse(uri); } catch (IOException ex) { LOGGER.warning("Could not open URL '" + uri + "': " + ex); } }); }).forEach(mi -> lookupURLs.getItems().add(mi)); changeMenu.getItems().add(lookupURLs); changeMenu.getItems().add(new SeparatorMenuItem()); changeMenu.getItems().add(createMenuItem("Prepend text to all notes", action -> { List<Change> changes = new ArrayList<>(changesTableView.getSelectionModel().getSelectedItems()); Optional<String> result = askUserForTextField( "Enter tags to prepend to notes in " + changes.size() + " changes:"); if (result.isPresent()) { String tags = result.get().trim(); changes.forEach(ch -> { String prevValue = change.getNote().orElse("").trim(); LOGGER.info("Prepending tags '" + tags + "' to previous value '" + prevValue + "' for change " + ch); ch.noteProperty().set((tags + " " + prevValue).trim()); }); } })); changeMenu.getItems().add(createMenuItem("Append text to all notes", action -> { List<Change> changes = new ArrayList<>(changesTableView.getSelectionModel().getSelectedItems()); Optional<String> result = askUserForTextField( "Enter tags to append to notes in " + changes.size() + " changes:"); if (result.isPresent()) { String tags = result.get().trim(); changes.forEach(ch -> { String prevValue = ch.getNote().orElse("").trim(); LOGGER.info("Appending tags '" + tags + "' to previous value '" + prevValue + "' for change " + ch); ch.noteProperty().setValue((prevValue + " " + tags).trim()); }); } })); changeMenu.show(datasetView.getScene().getWindow(), event.getScreenX(), event.getScreenY()); }); return row; }); LOGGER.info("setupTableWithChanges() completed"); } private MenuItem createMenuItem(String name, EventHandler<ActionEvent> handler) { MenuItem mi = new MenuItem(name); mi.onActionProperty().set(handler); return mi; } private Optional<String> askUserForTextField(String text) { TextField textfield = new TextField(); Dialog<ButtonType> dialog = new Dialog<>(); dialog.getDialogPane().headerTextProperty().set(text); dialog.getDialogPane().contentProperty().set(textfield); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); Optional<ButtonType> result = dialog.showAndWait(); if (result.isPresent() && result.get().equals(ButtonType.OK)) return Optional.of(textfield.getText()); else return Optional.empty(); } private Optional<String> askUserForTextArea(String label) { return askUserForTextArea(label, null); } private Optional<String> askUserForTextArea(String label, String initialText) { TextArea textarea = new TextArea(); if (initialText != null) textarea.setText(initialText); Dialog<ButtonType> dialog = new Dialog<>(); dialog.getDialogPane().headerTextProperty().set(label); dialog.getDialogPane().contentProperty().set(textarea); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); Optional<ButtonType> result = dialog.showAndWait(); if (result.isPresent() && result.get().equals(ButtonType.OK)) return Optional.of(textarea.getText()); else return Optional.empty(); } private void fillTableWithChanges(TableView<Change> tv, Dataset tp) { // Preserve search order and selected item. List<TableColumn<Change, ?>> sortByCols = new LinkedList<>(tv.getSortOrder()); List<Change> selectedChanges = new LinkedList<>(tv.getSelectionModel().getSelectedItems()); LOGGER.info("About to set changes table items: sortByCols = " + sortByCols + ", selectedChanges = " + selectedChanges); tv.setItems(FXCollections.observableList(tp.getAllChangesAsList())); LOGGER.info("tv.setItems() completed"); for (Change ch : selectedChanges) { tv.getSelectionModel().select(ch); } tv.getSortOrder().addAll(sortByCols); LOGGER.info("fillTableWithChanges() completed"); } public void selectChange(Change ch) { int row = changesTableView.getItems().indexOf(ch); if (row == -1) row = 0; LOGGER.info("Selecting change in row " + row + " (change " + ch + ")"); changesTableView.getSelectionModel().clearAndSelect(row); changesTableView.scrollTo(row); } // Export to CSV public List<List<String>> getDataAsTable(TableView tv) { // What columns do we have? List<List<String>> result = new LinkedList<>(); List<TableColumn> columns = tv.getColumns(); columns.forEach(col -> { List<String> column = new LinkedList<>(); // Add the header. column.add(col.getText()); // Add the data. for (int x = 0; x < tv.getItems().size(); x++) { ObservableValue cellObservableValue = col.getCellObservableValue(x); column.add(cellObservableValue.getValue().toString()); } result.add(column); }); return result; } private void fillCSVFormat(CSVFormat format, Appendable destination, List<List<String>> data) throws IOException { try (CSVPrinter printer = format.print(destination)) { List<List<String>> dataAsTable = data; if (dataAsTable.isEmpty()) return; for (int x = 0; x < dataAsTable.get(0).size(); x++) { for (int y = 0; y < dataAsTable.size(); y++) { String value = dataAsTable.get(y).get(x); printer.print(value); } printer.println(); } } } private void exportToCSV(TableView tv, ActionEvent evt) { FileChooser chooser = new FileChooser(); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("CSV file", "*.csv"), new FileChooser.ExtensionFilter("Tab-delimited file", "*.txt")); File file = chooser.showSaveDialog(datasetView.getStage()); if (file != null) { CSVFormat format = CSVFormat.RFC4180; String outputFormat = chooser.getSelectedExtensionFilter().getDescription(); if (outputFormat.equalsIgnoreCase("Tab-delimited file")) format = CSVFormat.TDF; try { List<List<String>> dataAsTable = getDataAsTable(tv); fillCSVFormat(format, new FileWriter(file), dataAsTable); Alert window = new Alert(Alert.AlertType.CONFIRMATION, "CSV file '" + file + "' saved with " + (dataAsTable.get(0).size() - 1) + " rows."); window.showAndWait(); } catch (IOException e) { Alert window = new Alert(Alert.AlertType.ERROR, "Could not save CSV to '" + file + "': " + e); window.showAndWait(); } } } @FXML private void exportChangesToCSV(ActionEvent evt) { exportToCSV(changesTableView, evt); } @FXML private void displayData(ActionEvent evt) { TabularDataViewController tdvc = TabularDataViewController.createTabularDataView(); // TODO: modify this so we can edit that data, too! tdvc.getHeaderTextProperty().set("Data contained in dataset " + dataset); // TODO we can search for names here, dude. fillTableViewWithDatasetRows(tdvc.getTableView()); Stage stage = new Stage(); stage.setTitle("Rows from " + dataset.asTitle()); stage.setScene(tdvc.getScene()); stage.show(); } private void fillTableViewWithDatasetRows(TableView<DatasetRow> tableView) { // We need to precalculate. ObservableList<DatasetRow> rows = dataset.rowsProperty(); // Setup table. tableView.editableProperty().set(false); ObservableList<TableColumn<DatasetRow, ?>> cols = tableView.getColumns(); cols.clear(); // Set up columns. TableColumn<DatasetRow, String> colRowName = new TableColumn<>("Name"); colRowName.setCellValueFactory((TableColumn.CellDataFeatures<DatasetRow, String> features) -> { DatasetRow row = features.getValue(); Set<Name> names = dataset.getNamesInRow(row); if (names.isEmpty()) { return new ReadOnlyStringWrapper("(None)"); } else { return new ReadOnlyStringWrapper( names.stream().map(n -> n.getFullName()).collect(Collectors.joining("; "))); } }); colRowName.setPrefWidth(100.0); cols.add(colRowName); // Create a column for every column here. dataset.getColumns().forEach((DatasetColumn col) -> { String colName = col.getName(); TableColumn<DatasetRow, String> colColumn = new TableColumn<>(colName); colColumn.setCellValueFactory((TableColumn.CellDataFeatures<DatasetRow, String> features) -> { DatasetRow row = features.getValue(); String val = row.get(colName); return new ReadOnlyStringWrapper(val == null ? "" : val); }); colColumn.setPrefWidth(100.0); cols.add(colColumn); }); // Set table items. tableView.itemsProperty().set(rows); // What if it's empty? tableView.setPlaceholder(new Label("No data contained in this dataset.")); } @FXML private void addNewChange(ActionEvent evt) { int selectedIndex = changesTableView.getSelectionModel().getSelectedIndex(); if (selectedIndex < 0) selectedIndex = 0; changesTableView.getItems().add(selectedIndex, new Change(dataset, ChangeType.ERROR, Stream.empty(), Stream.empty())); } @FXML private void deleteExplicitChange(ActionEvent evt) { List<Change> changesToDelete = new ArrayList<>(changesTableView.getSelectionModel().getSelectedItems()); List<Change> explicitChangesToDelete = changesToDelete.stream() .filter(ch -> !ch.getDataset().isChangeImplicit(ch)).collect(Collectors.toList()); if (explicitChangesToDelete.isEmpty()) return; // Explicit changes! Verify before deleting. Optional<ButtonType> opt = new Alert(AlertType.CONFIRMATION, "Are you sure you want to delete " + explicitChangesToDelete.size() + " explicit changes starting with " + explicitChangesToDelete.get(0).toString() + "? This cannot be undone!").showAndWait(); if (!opt.isPresent() || !opt.get().equals(ButtonType.OK)) return; // Okay, we're verified! Time to die. for (Change ch : explicitChangesToDelete) { ch.getDataset().deleteChange(ch); } } @FXML private void combineChanges(ActionEvent evt) { List<Change> changes = new ArrayList<>(changesTableView.getSelectionModel().getSelectedItems()); if (changes.size() < 2) { // Need two or more changes! return; } // Combine them changes! This means: // 1. Get the first change. // 2. For every subsequent change, add *everything* about that change to the first change, // then delete it. Change firstChange = null; Map<String, Set<String>> combinedProperties = new HashMap<>(); for (Change ch : changes) { if (firstChange == null) { firstChange = ch; // Set up the combined properties. combinedProperties = firstChange.getProperties().entrySet().stream().collect(Collectors.toMap( (Map.Entry<String, String> entry) -> entry.getKey(), (Map.Entry<String, String> entry) -> { HashSet<String> hs = new HashSet<String>(); hs.add(entry.getValue()); return hs; })); continue; } // Add 'from's to firstChange. firstChange.getFrom().addAll(ch.getFrom()); firstChange.getTo().addAll(ch.getTo()); firstChange.getCitations().addAll(ch.getCitations()); // Combine properties. for (Map.Entry<String, String> entry : ch.getProperties().entrySet()) { if (!combinedProperties.containsKey(entry.getKey())) combinedProperties.put(entry.getKey(), new HashSet<>()); combinedProperties.get(entry.getKey()).add(entry.getValue()); } // Done! dataset.deleteChange(ch); } // First change might be implicit! Make it explicit! dataset.makeChangeExplicit(firstChange); // Add all the combined properties back into firstChange. for (String key : combinedProperties.keySet()) { firstChange.getProperties().put(key, combinedProperties.get(key).stream().collect(Collectors.joining("; "))); } // Guess the new type. int fromCount = firstChange.getFrom().size(); int toCount = firstChange.getTo().size(); if (fromCount > 0 && toCount > 0) firstChange.typeProperty().setValue(ChangeType.RENAME); else if (fromCount > 0 && toCount == 0) firstChange.typeProperty().setValue(ChangeType.DELETION); else if (fromCount == 0 && toCount > 0) firstChange.typeProperty().setValue(ChangeType.ADDITION); else firstChange.typeProperty().setValue(ChangeType.ERROR); } @FXML private void refreshChanges(ActionEvent evt) { fillTableWithChanges(changesTableView, dataset); } @FXML private void divideChange(ActionEvent evt) { List<Change> changes = changesTableView.getSelectionModel().getSelectedItems(); for (Change ch : changes) { // Divide them changes! For our purposes, // this works like this: // - Remove all the 'froms' from the change. // - Create a new change identical to the first chnage, and // remove all its 'tos'. // TODO } } /* Some buttons are magic. */ @FXML private Button combineChangesButton; @FXML private Button divideChangeButton; @FXML private Button deleteExplicitChangeButton; private void setupMagicButtons() { // Disable everything to begin with. combineChangesButton.disableProperty().set(true); divideChangeButton.disableProperty().set(true); deleteExplicitChangeButton.disableProperty().set(true); // Switch them on and off based on the selection. changesTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Change>) ch -> { int countSelectionItems = ch.getList().size(); if (countSelectionItems == 0) { // No selection? None of those buttons should be on. combineChangesButton.disableProperty().set(true); divideChangeButton.disableProperty().set(true); deleteExplicitChangeButton.disableProperty().set(true); } else if (countSelectionItems == 1) { // Exactly one? We can split, but not combine. combineChangesButton.disableProperty().set(true); divideChangeButton.disableProperty().set(false); deleteExplicitChangeButton.disableProperty().set(false); } else { // More than one? We can combine, but not split. combineChangesButton.disableProperty().set(false); divideChangeButton.disableProperty().set(true); deleteExplicitChangeButton.disableProperty().set(false); } }); } /* * The additional data system. * * Here's how this works: * - Everything is wrapped up into an AdditionalData class. * - There's a bunch of code that knows how to convert AdditionalData objects into * list/table combinations. * - There's a separate bunch of code that builds AdditionalData objects. */ private class AdditionalData<ListOf, TableOf> { private String name; public String getName() { return name; } @Override public String toString() { return name; } private ObservableList<ListOf> listOf; private ObservableMap<ListOf, List<TableOf>> tableOf; private List<TableColumn<TableOf, String>> columns; private Function<List<Change>, List<ListOf>> onSelectChange; private AdditionalData(String name, List<ListOf> listOf, Map<ListOf, List<TableOf>> tableOfMap, List<TableColumn<TableOf, String>> columns) { this(name, listOf, tableOfMap, columns, null); } private AdditionalData(String name, List<ListOf> listOf, Map<ListOf, List<TableOf>> tableOfMap, List<TableColumn<TableOf, String>> columns, Function<List<Change>, List<ListOf>> onSelectChange) { this.name = name; this.listOf = FXCollections.observableList(listOf); this.tableOf = FXCollections.observableMap(tableOfMap); this.columns = FXCollections.observableList(columns); this.onSelectChange = onSelectChange; } public List<ListOf> getList() { return listOf; } public List<TableOf> getTableRowsFor(ListOf listOfItem) { return tableOf.getOrDefault(listOfItem, new ArrayList<>()); } public List<TableColumn<TableOf, String>> getColumns() { return columns; } public void onSelectChange(List<Change> selectedChanges) { if (onSelectChange == null) return; additionalListView.getSelectionModel().clearSelection(); List<ListOf> listOfs = onSelectChange.apply(selectedChanges); if (listOfs.isEmpty()) return; for (ListOf lo : listOfs) { additionalListView.getSelectionModel().select(lo); } // Scroll to the first name. additionalListView.scrollTo(listOfs.get(0)); } } @SuppressWarnings("rawtypes") @FXML private TableView additionalDataTableView; @SuppressWarnings("rawtypes") @FXML private ListView additionalListView; @SuppressWarnings("rawtypes") @FXML private ComboBox<AdditionalData> additionalDataCombobox; @SuppressWarnings("rawtypes") private ObservableList tableItems = FXCollections.observableList(new LinkedList()); // The following methods switch between additional data views. @SuppressWarnings("unchecked") private void initAdditionalData() { // Resize to fit columns, as per https://stackoverflow.com/a/22488513/27310 additionalDataTableView.setColumnResizePolicy((param) -> true); // Set up additional data objects. additionalDataTableView.setRowFactory(table -> { @SuppressWarnings("rawtypes") TableRow row = new TableRow<>(); row.setOnMouseClicked(event -> { if (row.isEmpty()) return; Object item = row.getItem(); if (event.getClickCount() == 2) { // Try opening the detailed view on this item -- if we can. datasetView.getProjectView().openDetailedView(item); } }); return row; }); additionalDataTableView.setItems(new SortedList<>(tableItems)); // Set up events. additionalDataCombobox.getSelectionModel().selectedItemProperty() .addListener((Observable o) -> additionalDataUpdateList()); additionalListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); additionalListView.getSelectionModel().selectedItemProperty() .addListener((Observable o) -> additionalDataUpdateTable()); additionalDataCombobox.getSelectionModel().select(0); // When the change is changed, select an item. changesTableView.getSelectionModel().getSelectedItems() .addListener((ListChangeListener<Change>) c -> additionalDataUpdateList()); } private void additionalDataUpdateList() { // Which AdditionalData and ListOf are we in right now? AdditionalData aData = additionalDataCombobox.getSelectionModel().getSelectedItem(); // No aData? Do nothing! if (aData == null) return; // Object currentSelection = additionalListView.getSelectionModel().getSelectedItem(); additionalListView.setItems(FXCollections.observableList(aData.getList())); additionalListView.getSelectionModel().clearAndSelect(0); // This is also the right time to set up columns for the table. additionalDataTableView.getColumns().clear(); additionalDataTableView.getColumns().addAll(aData.getColumns()); // additionalListView.getSelectionModel().select(prevSelection); } private void additionalDataUpdateTable() { // Which AdditionalData and ListOf are we in right now? AdditionalData aData = additionalDataCombobox.getSelectionModel().getSelectedItem(); // Redraw the table. tableItems.clear(); tableItems.addAll(aData.getTableRowsFor(additionalListView.getSelectionModel().getSelectedItem())); } // The following AdditionalData objects provide all the additional data views we need. @SuppressWarnings("rawtypes") private void updateAdditionalData() { ObservableList<AdditionalData> addDataItems = FXCollections.observableArrayList(); // Done! additionalDataCombobox.setItems(addDataItems); // We can just about get away with doing this for around ADDITIONAL_DATA_CHANGE_COUNT_LIMIT changes. if (dataset.getAllChanges().count() > ADDITIONAL_DATA_CHANGE_COUNT_LIMIT) return; // TODO: fix this by lazy-evaluating these durned lists. // 1. Changes by name LOGGER.info("Creating changes by name additional data"); addDataItems.add(createChangesByNameAdditionalData()); LOGGER.info("Finished changes by name additional data"); // 2. Data by name LOGGER.info("Creating data by name additional data"); addDataItems.add(createDataByNameAdditionalData()); LOGGER.info("Finished changes by name additional data"); // 3. Changes by subname LOGGER.info("Creating changes by subnames additional data"); addDataItems.add(createChangesBySubnamesAdditionalData()); LOGGER.info("Finished changes by subname additional data"); // 4. Data in this dataset /* LOGGER.info("Creating data by name additional data"); addDataItems.add(createDataAdditionalData()); LOGGER.info("Finished changes by name additional data"); */ // 5. Properties LOGGER.info("Creating properties additional data"); addDataItems.add(createPropertiesAdditionalData()); LOGGER.info("Finished properties additional data"); additionalDataCombobox.getSelectionModel().clearAndSelect(0); LOGGER.info("Finished updateAdditionalData()"); } private AdditionalData<String, DatasetRow> createDataAdditionalData() { Map<String, List<DatasetRow>> map = new HashMap<>(); map.put("All data (" + dataset.getRowCount() + " rows)", new ArrayList<DatasetRow>(dataset.rowsProperty())); List<TableColumn<DatasetRow, String>> cols = new LinkedList<>(); for (DatasetColumn col : dataset.getColumns()) { TableColumn<DatasetRow, String> column = new TableColumn<>(col.getName()); column.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().get(col))); cols.add(column); } return new AdditionalData("Data", Arrays.asList("All data (" + dataset.getRowCount() + " rows)"), map, cols); } private AdditionalData<String, Map.Entry<String, String>> createPropertiesAdditionalData() { List<Map.Entry<String, String>> datasetProperties = new ArrayList<>(dataset.getProperties().entrySet()); Map<String, List<Map.Entry<String, String>>> map = new HashMap<>(); map.put("Dataset (" + datasetProperties.size() + ")", datasetProperties); List<TableColumn<Map.Entry<String, String>, String>> cols = new ArrayList<>(); TableColumn<Map.Entry<String, String>, String> colKey = new TableColumn<>("Key"); colKey.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().getKey())); cols.add(colKey); TableColumn<Map.Entry<String, String>, String> colValue = new TableColumn<>("Value"); colValue.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().getValue())); cols.add(colValue); return new AdditionalData("Properties", Arrays.asList("Dataset (" + datasetProperties.size() + ")"), map, cols); } private AdditionalData<Name, Map.Entry<String, String>> createDataByNameAdditionalData() { // Which names area we interested in? List<Change> selectedChanges = changesTableView.getItems(); List<Name> names = selectedChanges.stream().flatMap(ch -> { Set<Name> allNames = ch.getAllNames(); List<Name> binomials = allNames.stream().flatMap(n -> n.asBinomial()).collect(Collectors.toList()); List<Name> genus = allNames.stream().flatMap(n -> n.asGenus()).collect(Collectors.toList()); allNames.addAll(binomials); allNames.addAll(genus); return allNames.stream(); }).distinct().sorted().collect(Collectors.toList()); Project proj = datasetView.getProjectView().getProject(); Map<Name, List<Map.Entry<String, String>>> map = new HashMap<>(); for (Name n : names) { Map<DatasetColumn, Set<String>> dataForName = proj.getDataForName(n); Map<String, String> mapForName = dataForName.entrySet().stream() .collect(Collectors.toMap( (Map.Entry<DatasetColumn, Set<String>> entry) -> entry.getKey().toString(), (Map.Entry<DatasetColumn, Set<String>> entry) -> entry.getValue().toString())); map.put(n, new ArrayList<>(mapForName.entrySet())); } List<TableColumn<Map.Entry<String, String>, String>> cols = new ArrayList<>(); TableColumn<Map.Entry<String, String>, String> colKey = new TableColumn<>("Key"); colKey.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().getKey())); cols.add(colKey); TableColumn<Map.Entry<String, String>, String> colValue = new TableColumn<>("Value"); colValue.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().getValue())); cols.add(colValue); return new AdditionalData<Name, Map.Entry<String, String>>("Data by name", names, map, cols, changes -> changes.stream().flatMap(ch -> ch.getAllNames().stream()).collect(Collectors.toList())); } private TableColumn<Change, String> getChangeTableColumn(String colName, Function<Change, String> func) { TableColumn<Change, String> col = new TableColumn<>(colName); col.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(func.apply(cdf.getValue()))); return col; } private AdditionalData<Name, Change> createChangesByNameAdditionalData() { // Which names area we interested in? List<Change> selectedChanges = changesTableView.getItems(); List<Name> names = selectedChanges.stream().flatMap(ch -> { Set<Name> allNames = ch.getAllNames(); List<Name> binomials = allNames.stream().flatMap(n -> n.asBinomial()).collect(Collectors.toList()); List<Name> genus = allNames.stream().flatMap(n -> n.asGenus()).collect(Collectors.toList()); allNames.addAll(binomials); allNames.addAll(genus); return allNames.stream(); }).distinct().sorted().collect(Collectors.toList()); Project proj = datasetView.getProjectView().getProject(); Map<Name, List<Change>> map = new HashMap<>(); for (Name n : names) { map.put(n, proj.getDatasets().stream().flatMap(ds -> ds.getAllChanges()) .filter(ch -> ch.getAllNames().contains(n)).collect(Collectors.toList())); } List<TableColumn<Change, String>> cols = new ArrayList<>(); cols.add(getChangeTableColumn("Dataset", ch -> ch.getDataset().toString())); cols.add(getChangeTableColumn("Type", ch -> ch.getType().toString())); cols.add(getChangeTableColumn("From", ch -> ch.getFrom().toString())); cols.add(getChangeTableColumn("To", ch -> ch.getTo().toString())); cols.add(getChangeTableColumn("Note", ch -> ch.getNote().orElse(""))); return new AdditionalData<Name, Change>("Changes by name", names, map, cols, changes -> changes.stream().flatMap(ch -> ch.getAllNames().stream()).collect(Collectors.toList())); } private AdditionalData<Name, Change> createChangesBySubnamesAdditionalData() { // Which names area we interested in? List<Change> selectedChanges = changesTableView.getItems(); List<Name> names = selectedChanges.stream().flatMap(ch -> { Set<Name> allNames = ch.getAllNames(); List<Name> binomials = allNames.stream().flatMap(n -> n.asBinomial()).collect(Collectors.toList()); List<Name> genus = allNames.stream().flatMap(n -> n.asGenus()).collect(Collectors.toList()); allNames.addAll(binomials); allNames.addAll(genus); return allNames.stream(); }).distinct().sorted().collect(Collectors.toList()); Project proj = datasetView.getProjectView().getProject(); Map<Name, List<Change>> map = new HashMap<>(); for (Name query : names) { map.put(query, proj.getDatasets().stream().flatMap(ds -> ds.getAllChanges()) .filter(ch -> ch.getAllNames().contains(query) || ch.getAllNames().stream().flatMap(n -> n.asBinomial()) .anyMatch(binomial -> query.equals(binomial)) || ch.getAllNames().stream().flatMap(n -> n.asGenus()) .anyMatch(genus -> query.equals(genus))) .collect(Collectors.toList())); } List<TableColumn<Change, String>> cols = new ArrayList<>(); cols.add(getChangeTableColumn("Dataset", ch -> ch.getDataset().toString())); cols.add(getChangeTableColumn("Type", ch -> ch.getType().toString())); cols.add(getChangeTableColumn("From", ch -> ch.getFrom().toString())); cols.add(getChangeTableColumn("To", ch -> ch.getTo().toString())); cols.add(getChangeTableColumn("Note", ch -> ch.getNote().orElse(""))); return new AdditionalData<Name, Change>("Changes by subname", names, map, cols, changes -> changes.stream().flatMap(ch -> ch.getAllNames().stream()).collect(Collectors.toList())); } }