Newer
Older
jrex / src / main / java / jrex / ui / model / PivotTableModel.java
package jrex.ui.model;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.table.AbstractTableModel;
import jrex.ui.NonUniqueKeyException;

/**
 * A table model that can be pivoted.
 *
 * @author Mark George <mark.george@otago.ac.nz>
 */
public class PivotTableModel extends AbstractTableModel {

   private Table<Integer, Integer, String> table = HashBasedTable.create();
   private Map<Integer, String> columnHeadings = new LinkedHashMap<>();
   private Map<String, Integer> headingIndexes = new HashMap<>();

   @Override
   public int getRowCount() {
      return table.rowKeySet().size();
   }

   @Override
   public int getColumnCount() {
      return table.columnKeySet().size();
   }

   public void setColumnHeadings(String[] headings) {
      columnHeadings.clear();
      for (int i = 0; i < headings.length; i++) {
         columnHeadings.put(i + 1, headings[i]);
         headingIndexes.put(headings[i], i+1);
      }
   }

   @Override
   public String getColumnName(int columnIndex) {
      if (columnHeadings.isEmpty()) {
         // no explicit headings, so use column index
         return String.valueOf(columnIndex + 1);
      } else {
         return columnHeadings.get(columnIndex + 1);
      }
   }

   public Integer getHeadingIndex(String heading) {
      return headingIndexes.get(heading);
   }

   @Override
   public Class<?> getColumnClass(int columnIndex) {
      return Object.class;
   }

   @Override
   public boolean isCellEditable(int rowIndex, int columnIndex) {
      return false;
   }

   @Override
   public Object getValueAt(int rowIndex, int columnIndex) {
      return table.get(rowIndex, columnIndex);
   }

   @Override
   public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
      table.put(rowIndex, columnIndex, aValue.toString());
      fireTableCellUpdated(rowIndex, columnIndex);
   }

   public void addRow(String[] values) {
      int rowCount = getRowCount();
      for (int i = 0; i < values.length; i++) {
         table.put(rowCount, i, values[i]);
      }
      fireTableRowsInserted(rowCount, rowCount);
   }

   public List<String> getHeadings() {
      if (columnHeadings.isEmpty()) {
         for (int i=0; i < table.columnKeySet().size(); i++) {
            columnHeadings.put(i+1, String.valueOf(i+1));
            headingIndexes.put(String.valueOf(i+1), i+1);
         }
      }
      return new ArrayList(columnHeadings.values());
   }

   public void transpose() {
      table = Tables.transpose(table);
      fireTableStructureChanged();
   }

   public void pivotWideToLong(List<String> preservedColumns, List<String> valueColumns) {
      Table<Integer, Integer, String> pivoted = HashBasedTable.create();

      //### create multimaps so that we can easily get the data by header ###//
      ListMultimap<String, String> allMap = ArrayListMultimap.create();

      for (Map.Entry<Integer, String> col : columnHeadings.entrySet()) {
         for (Integer row : table.rowKeySet()) {
            allMap.put(col.getValue(), table.get(row, col.getKey() - 1));
         }
      }

      // create multimap for preserved columns only
      ListMultimap<String, String> preservedMap = ArrayListMultimap.create();
      for (String preservedColumn : preservedColumns) {
         preservedMap.putAll(preservedColumn, allMap.get(preservedColumn));
      }

      // create multimap for value columns only
      ListMultimap<String, String> valueMap = ArrayListMultimap.create();
      for (String valueColumn : valueColumns) {
         valueMap.putAll(valueColumn, allMap.get(valueColumn));
      }

      //### add headers to pivoted table ###//
      int numHeaders = preservedColumns.size() + 2;
      String[] headings = new String[numHeaders];
      int col = 0;

      // preserved column headers
      for (String preservedColumn : preservedColumns) {
         headings[col] = preservedColumn;
         col++;
      }

      headings[col] = "Key";
      headings[col + 1] = "Value";

      setColumnHeadings(headings);

      int row = 0;
      int pCol = 0;
      int pColSize = preservedColumns.size();
      int index = 0;  // original row index

      //### add preserved columns ###/
      for (String preservedCol : preservedColumns) {
         for (String preservedVal : preservedMap.get(preservedCol)) {
            for (String valueColumn : valueColumns) {
               // write preserved column
               pivoted.put(row, pCol, preservedVal);

               // write value columns
               List<String> values = new ArrayList<>(valueMap.get(valueColumn));
               String value = values.get(index);

               pivoted.put(row, pColSize, valueColumn);
               pivoted.put(row++, pColSize + 1, value);
            }
            index++;
         }
         index = 0;
         row = 0;
         pCol++;
      }

      table = pivoted;
      fireTableStructureChanged();
   }

   public void pivotLongToWide(List<String> repeatingColumns, String headerColumn, String valueColumn) throws NonUniqueKeyException {
      //### create multimaps so that we can easily get the data by header ###//
      ListMultimap<String, String> allMap = ArrayListMultimap.create();

      for (Map.Entry<Integer, String> col : columnHeadings.entrySet()) {
         for (Integer row : table.rowKeySet()) {
            allMap.put(col.getValue(), table.get(row, col.getKey() - 1));
         }
      }

      Table<String, String, String> unique = HashBasedTable.create();

      
      for (Integer rowIdx : table.rowKeySet()) {
         List<String> columnVals = new ArrayList<>();

         StringBuilder key = new StringBuilder();
         for (String repeatingColumn : repeatingColumns) {
            key.append(allMap.get(repeatingColumn).get(rowIdx));
         }

         for (String repeatingColumn : repeatingColumns) {
            unique.put(key.toString(), repeatingColumn, allMap.get(repeatingColumn).get(rowIdx));
            columnVals.add(allMap.get(repeatingColumn).get(rowIdx));
         }

         if(unique.contains(key.toString(), allMap.get(headerColumn).get(rowIdx))) {
            throw new NonUniqueKeyException("There is already a value for [" + allMap.get(headerColumn).get(rowIdx) + "] for " + columnVals +".\n\nAborting pivot since data will be lost.");
         }
         
         unique.put(key.toString(), allMap.get(headerColumn).get(rowIdx), allMap.get(valueColumn).get(rowIdx));

      }

      // create a new indexed table for the table model
      Table<Integer, Integer, String> pivoted = HashBasedTable.create();

      int rowIdx = 0;

      for (Map.Entry<String, Map<String, String>> row : unique.rowMap().entrySet()) {

         if (rowIdx == 0) { // write headers
            Set<String> colSet = row.getValue().keySet();
            setColumnHeadings(colSet.toArray(new String[colSet.size()]));
         }

         int colIdx = 0;

         for (Map.Entry<String, String> entry : row.getValue().entrySet()) {

            pivoted.put(rowIdx, colIdx, entry.getValue());
            colIdx++;
         }
         rowIdx++;
      }

      table = pivoted;
      fireTableStructureChanged();
   }

   public Table<Integer, Integer, String> getTable() {
      return table;
   }

}