com.ggvaidya.scinames.ui.BulkChangeEditorController.java Source code

Java tutorial

Introduction

Here is the source code for com.ggvaidya.scinames.ui.BulkChangeEditorController.java

Source

/*
 * 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.ui;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;

import com.ggvaidya.scinames.SciNames;
import com.ggvaidya.scinames.model.ChangeType;
import com.ggvaidya.scinames.model.Dataset;
import com.ggvaidya.scinames.model.DatasetColumn;
import com.ggvaidya.scinames.model.Name;
import com.ggvaidya.scinames.model.Project;
import com.ggvaidya.scinames.model.change.ChangeGenerator;
import com.ggvaidya.scinames.model.change.ChangeTypeStringConverter;
import com.ggvaidya.scinames.model.change.GenusChangesFromComposition;
import com.ggvaidya.scinames.model.change.GenusReorganizationFromRenames;
import com.ggvaidya.scinames.model.change.NameSetStringConverter;
import com.ggvaidya.scinames.model.change.PotentialChange;
import com.ggvaidya.scinames.model.change.RenamesFromIdsInChanges;
import com.ggvaidya.scinames.model.change.RenamesFromIdsInData;
import com.ggvaidya.scinames.model.change.SynonymsFromColumnChangeGenerator;
import com.ggvaidya.scinames.model.filters.ChangeFilter;
import com.ggvaidya.scinames.util.SimplifiedDate;

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.ObservableSet;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
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.scene.input.MouseButton;
import javafx.stage.FileChooser;
import javafx.util.StringConverter;

/**
 * FXML Controller class for bulk-creating changes using different methods.
 *
 * @author Gaurav Vaidya <gaurav@ggvaidya.com>
 */
public class BulkChangeEditorController {
    private static final Logger LOGGER = Logger.getLogger(BulkChangeEditor.class.getSimpleName());
    private static final Dataset ALL = new Dataset("All datasets", SimplifiedDate.MIN, Dataset.TYPE_DATASET);

    private BulkChangeEditor bulkChangeEditor;
    private Project project;

    public BulkChangeEditorController() {
    }

    public void setBulkChangeEditor(BulkChangeEditor bce) {
        bulkChangeEditor = bce;
        project = bce.getProjectView().getProject();

        List<DatasetColumn> columns = project.getDatasets().stream().flatMap(ds -> ds.getColumns().stream())
                .distinct().collect(Collectors.toList());
        comboBoxNameIdentifiers.setItems(FXCollections.observableArrayList(columns));

        comboBoxMethods.getSelectionModel().selectedItemProperty().addListener((a, b, c) -> {
            ChangeGenerator generator = comboBoxMethods.getSelectionModel().getSelectedItem();

            if (generator.needsDatasetColumn()) {
                // Activate it if this method needs a column
                comboBoxNameIdentifiers.setDisable(false);
            } else {
                // Otherwise 
                comboBoxNameIdentifiers.setDisable(true);
            }
        });

        ObservableList<Dataset> datasets = FXCollections.observableArrayList();
        datasets.add(ALL);
        datasets.addAll(project.getDatasets());
        datasetsComboBox.setItems(datasets);
        datasetsComboBox.getSelectionModel().clearAndSelect(0);

        setupChangesTableView();
        // findChanges();
    }

    /**
     * Initializes the controller class.
     */
    public void initialize() {
        comboBoxMethods.setEditable(false);
        comboBoxMethods.setConverter(new StringConverter<ChangeGenerator>() {

            @Override
            public String toString(ChangeGenerator generator) {
                return generator.getName();
            }

            @Override
            public ChangeGenerator fromString(String string) {
                return null;
            }

        });
        comboBoxMethods.setItems(availableMethods);
        comboBoxMethods.getSelectionModel().clearAndSelect(0);
        changesTableView.setOnMouseClicked(ms -> {
            if (ms.getClickCount() == 2 && ms.getButton().equals(MouseButton.PRIMARY)) {
                selectChange(changesTableView.getSelectionModel().getSelectedItem());
            }
        });

        foundChanges.addListener((ListChangeListener) evt -> {
            statusTextField.setText(foundChanges.size() + " changes generated from "
                    + foundChanges.stream().map(ch -> ch.getDataset()).distinct().count() + " datasets");
        });
    }

    /*
     * User interface.
     */
    @FXML
    private ComboBox<ChangeGenerator> comboBoxMethods;
    @FXML
    private ComboBox<DatasetColumn> comboBoxNameIdentifiers;
    @FXML
    private ComboBox<Dataset> datasetsComboBox;
    @FXML
    private TableView<PotentialChange> changesTableView;
    @FXML
    private TextField statusTextField;

    /*
     * Methods for finding changes
     */

    private final ObservableList<ChangeGenerator> availableMethods = FXCollections
            .observableArrayList(Arrays.asList(new RenamesFromIdsInChanges(), new RenamesFromIdsInData(),
                    new SynonymsFromColumnChangeGenerator(),
                    // new GenusChangesFromComposition(),
                    new GenusReorganizationFromRenames()));

    private ObservableList<PotentialChange> foundChanges = FXCollections.observableList(new LinkedList<>());

    @FXML
    public void findChanges() {
        // Clear existing.
        foundChanges.clear();

        // Which datasets are we working on?
        Dataset dataset = datasetsComboBox.getValue();

        // Which method should we use?
        ChangeGenerator generator = comboBoxMethods.getSelectionModel().getSelectedItem();
        if (generator == null)
            return;

        if (generator.needsDatasetColumn())
            generator.setDatasetColumn(comboBoxNameIdentifiers.getSelectionModel().getSelectedItem());

        if (dataset == ALL) {
            foundChanges.setAll(generator.generate(project).collect(Collectors.toList()));
        } else {
            foundChanges.setAll(generator.generate(project, dataset).collect(Collectors.toList()));
        }
    }

    private void setupChangesTableView() {
        changesTableView.setEditable(true);
        changesTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        changesTableView.getColumns().clear();

        TableColumn<PotentialChange, ChangeType> colChangeType = new TableColumn<>("Type");
        colChangeType.setCellFactory(ComboBoxTableCell.forTableColumn(new ChangeTypeStringConverter(),
                ChangeType.ADDITION, ChangeType.DELETION, ChangeType.RENAME, ChangeType.LUMP, ChangeType.SPLIT,
                ChangeType.ERROR));
        colChangeType.setCellValueFactory(new PropertyValueFactory<>("type"));
        colChangeType.setPrefWidth(100.0);
        colChangeType.setEditable(true);
        changesTableView.getColumns().add(colChangeType);

        TableColumn<PotentialChange, ObservableSet<Name>> colChangeFrom = new TableColumn<>("From");
        colChangeFrom.setCellFactory(TextFieldTableCell.forTableColumn(new NameSetStringConverter()));
        colChangeFrom.setCellValueFactory(new PropertyValueFactory<>("from"));
        colChangeFrom.setPrefWidth(200.0);
        colChangeFrom.setEditable(true);
        changesTableView.getColumns().add(colChangeFrom);

        TableColumn<PotentialChange, ObservableSet<Name>> colChangeTo = new TableColumn<>("To");
        colChangeTo.setCellFactory(TextFieldTableCell.forTableColumn(new NameSetStringConverter()));
        colChangeTo.setCellValueFactory(new PropertyValueFactory<>("to"));
        colChangeTo.setPrefWidth(200.0);
        colChangeTo.setEditable(true);
        changesTableView.getColumns().add(colChangeTo);

        TableColumn<PotentialChange, String> colChangeDataset = new TableColumn<>("Dataset");
        colChangeDataset.setCellValueFactory(new PropertyValueFactory<>("dataset"));
        colChangeDataset.setPrefWidth(100.0);
        changesTableView.getColumns().add(colChangeDataset);

        ChangeFilter cf = project.getChangeFilter();
        TableColumn<PotentialChange, String> colFiltered = new TableColumn<>("Eliminated by filter?");
        colFiltered.setCellValueFactory(
                (TableColumn.CellDataFeatures<PotentialChange, String> features) -> new ReadOnlyStringWrapper(
                        cf.test(features.getValue()) ? "Allowed" : "Eliminated"));
        changesTableView.getColumns().add(colFiltered);

        TableColumn<PotentialChange, String> colNote = new TableColumn<>("Note");
        colNote.setCellFactory(TextFieldTableCell.forTableColumn());
        colNote.setCellValueFactory(new PropertyValueFactory<>("note"));
        colNote.setPrefWidth(100.0);
        colNote.setEditable(true);
        changesTableView.getColumns().add(colNote);

        TableColumn<PotentialChange, String> colProperties = new TableColumn<>("Properties");
        colProperties.setCellValueFactory(
                (TableColumn.CellDataFeatures<PotentialChange, String> features) -> new ReadOnlyStringWrapper(
                        features.getValue().getProperties().entrySet().stream()
                                .map(entry -> entry.getKey() + ": " + entry.getValue()).sorted()
                                .collect(Collectors.joining("; "))));
        changesTableView.getColumns().add(colProperties);

        TableColumn<PotentialChange, String> colCitations = new TableColumn<>("Citations");
        colCitations.setCellValueFactory(
                (TableColumn.CellDataFeatures<PotentialChange, String> features) -> new ReadOnlyStringWrapper(
                        features.getValue().getCitationStream().map(citation -> citation.getCitation()).sorted()
                                .collect(Collectors.joining("; "))));
        changesTableView.getColumns().add(colCitations);

        TableColumn<PotentialChange, String> colGenera = new TableColumn<>("Genera");
        colGenera.setCellValueFactory(
                (TableColumn.CellDataFeatures<PotentialChange, String> features) -> new ReadOnlyStringWrapper(
                        String.join(", ", features.getValue().getAllNames().stream().map(n -> n.getGenus())
                                .distinct().sorted().collect(Collectors.toList()))));
        changesTableView.getColumns().add(colGenera);

        TableColumn<PotentialChange, String> colSpecificEpithet = new TableColumn<>("Specific epithet");
        colSpecificEpithet.setCellValueFactory(
                (TableColumn.CellDataFeatures<PotentialChange, String> features) -> new ReadOnlyStringWrapper(String
                        .join(", ", features.getValue().getAllNames().stream().map(n -> n.getSpecificEpithet())
                                .filter(s -> s != null).distinct().sorted().collect(Collectors.toList()))));
        changesTableView.getColumns().add(colSpecificEpithet);

        // TODO: if we can get an ObservableList over tp.getAllChanges(), then this table
        // will update dynamically as changes are made. Won't that be something.
        // Yes, we want to getAllChanges() so we can see which ones are filtered out.
        changesTableView.setItems(foundChanges);
        changesTableView.getSortOrder().add(colChangeType);
    }

    public void selectChange(PotentialChange ch) {
        int row = changesTableView.getItems().indexOf(ch);
        if (row == -1)
            row = 0;

        LOGGER.fine("Selecting change in row " + row + " (change " + ch + ")");

        changesTableView.getSelectionModel().clearAndSelect(row);
        changesTableView.scrollTo(row);
    }

    @FXML
    private void backupCurrentDataset(ActionEvent evt) {
        // We just need to save this somewhere that isn't the project's actual file location.
        File currentFile = project.getFile();

        FileChooser chooser = new FileChooser();
        chooser.setTitle("Save project to ...");
        chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Project XML.gz file", "*.xml.gz"));
        File f = chooser.showSaveDialog(bulkChangeEditor.getStage());
        if (f != null) {
            project.setFile(f);

            try {
                SciNames.reportMemoryStatus("Saving project " + project + " to disk");
                project.saveToFile();
                SciNames.reportMemoryStatus("Project saved to disk");

                new Alert(Alert.AlertType.INFORMATION,
                        "Project saved as " + f + "; subsequent saves will return to " + currentFile).showAndWait();
            } catch (IOException ex) {
                new Alert(Alert.AlertType.ERROR, "Could not save project to file '" + f + "': " + ex).showAndWait();
            }
        }

        project.setFile(currentFile);
    }

    @FXML
    private void addSelectedChanges(ActionEvent evt) {
        foundChanges.stream().forEach(ch -> {
            ch.getDataset().explicitChangesProperty().add(ch);
        });

        new Alert(Alert.AlertType.INFORMATION, foundChanges.size() + " changes added to the project!")
                .showAndWait();

        foundChanges.clear();
    }

    /**
     * Provide an export of the data in the TableView as a "table". In its
     * simplest Java representation, that is a list of columns, with each
     * column starting with a column header and then all the rest of the data.
     * 
     * Warning: this can be a long-running function!
     * 
     * @return A list of columns of data.
     */
    public List<List<String>> getDataAsTable() {
        // What columns do we have?
        List<List<String>> result = new LinkedList<>();
        List<TableColumn<PotentialChange, ?>> columns = changesTableView.getColumns();

        columns.forEach(col -> {
            List<String> column = new LinkedList<>();

            // Add the header.
            column.add(col.getText());

            // Add the data.
            for (int x = 0; x < changesTableView.getItems().size(); x++) {
                ObservableValue cellObservableValue = col.getCellObservableValue(x);
                Object val = cellObservableValue.getValue();
                if (val == null)
                    column.add("NA");
                else
                    column.add(val.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();
            }
        }
    }

    @FXML
    private void exportToCSV(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(bulkChangeEditor.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();
                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();
            }
        }
    }
}