/*
* Copyright (C) 2014 by Array Systems Computing Inc. http://www.array.ca
*
* 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 org.esa.snap.productlibrary.rcp.toolviews.model;
import org.apache.commons.math3.util.FastMath;
import org.esa.snap.core.util.Guardian;
import org.esa.snap.engine_utilities.db.ProductEntry;
import org.esa.snap.productlibrary.rcp.toolviews.model.dataprovider.DataProvider;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortingDecorator extends AbstractTableModel {
private static final int DESCENDING = -1;
private static final int NOT_SORTED = 0;
private static final int ASCENDING = 1;
private static final Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
private final ProductEntryTableModel tableModel;
private final JTableHeader _tableHeader;
private final List<Directive> sortingColumns = new ArrayList<>();
private Row[] viewToModel = null;
private boolean doSortBy = false;
public enum SORT_BY {NAME, DATE, MISSON, TYPE, FILESIZE}
private SORT_BY sortBy;
private int sortedByDirection = NOT_SORTED;
public SortingDecorator(final ProductEntryTableModel tableModel, final JTableHeader tableHeader) {
Guardian.assertNotNull("tableModel", tableModel);
Guardian.assertNotNull("tableHeader", tableHeader);
this.tableModel = tableModel;
this.tableModel.addTableModelListener(new TableModelListener() {
public void tableChanged(final TableModelEvent e) {
initViewToModel();
fireTableChanged(e);
}
});
_tableHeader = tableHeader;
_tableHeader.addMouseListener(new MouseHandler());
_tableHeader.setDefaultRenderer(new SortableHeaderRenderer(_tableHeader.getDefaultRenderer()));
}
public int getRowCount() {
return tableModel.getRowCount();
}
public int getColumnCount() {
return tableModel.getColumnCount();
}
public Object getValueAt(final int rowIndex, final int columnIndex) {
return tableModel.getValueAt(getSortedIndex(rowIndex), columnIndex);
}
@Override
public String getColumnName(final int column) {
return tableModel.getColumnName(column);
}
@Override
public Class getColumnClass(final int column) {
return tableModel.getColumnClass(column);
}
@Override
public boolean isCellEditable(final int row, final int column) {
return tableModel.isCellEditable(getSortedIndex(row), column);
}
@Override
public void setValueAt(final Object aValue, final int row, final int column) {
tableModel.setValueAt(aValue, getSortedIndex(row), column);
}
public int getSortedIndex(final int rowIndex) {
final Row[] view = getViewToModel();
if (rowIndex < view.length) {
return view[rowIndex].modelIndex;
} else if (view.length > 0) {
return view[0].modelIndex;
}
return 0;
}
private Row[] getViewToModel() {
if (viewToModel == null) {
initViewToModel();
}
return viewToModel;
}
private void initViewToModel() {
final int tableModelRowCount = tableModel.getRowCount();
viewToModel = new Row[tableModelRowCount];
for (int row = 0; row < tableModelRowCount; row++) {
viewToModel[row] = new Row(row);
}
if (isSorting()) {
Arrays.sort(getViewToModel());
}
}
boolean isSorting() {
return !sortingColumns.isEmpty() || doSortBy;
}
public void sortBy(final SORT_BY val) {
doSortBy = true;
sortedByDirection = sortBy == val ? DESCENDING : ASCENDING;
sortBy = val;
initViewToModel();
fireTableDataChanged();
}
private class MouseHandler extends MouseAdapter {
@Override
public void mouseClicked(final MouseEvent e) {
final JTableHeader h = (JTableHeader) e.getSource();
final TableColumnModel columnModel = h.getColumnModel();
final int viewColumnIndex = columnModel.getColumnIndexAtX(e.getX());
final int columnIndex = columnModel.getColumn(viewColumnIndex).getModelIndex();
if (columnIndex != -1) {
int direction = getSortingDirection(columnIndex);
if (!e.isControlDown()) {
clearSortingDirections();
_tableHeader.repaint();
}
// Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
// {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
direction += (e.isShiftDown() ? -1 : 1);
direction = (direction + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
setDirectionForColumn(columnIndex, direction);
doSortBy = false;
initViewToModel();
fireTableDataChanged();
}
}
}
private void setDirectionForColumn(final int column, final int direction) {
final Directive directive = getDirective(column);
if (directive != EMPTY_DIRECTIVE) {
sortingColumns.remove(directive);
}
if (direction != NOT_SORTED) {
sortingColumns.add(new Directive(column, direction));
}
}
private class SortableHeaderRenderer implements TableCellRenderer {
private final TableCellRenderer tableCellRenderer;
public SortableHeaderRenderer(final TableCellRenderer tableCellRenderer) {
this.tableCellRenderer = tableCellRenderer;
}
public Component getTableCellRendererComponent(final JTable table,
final Object value,
final boolean isSelected,
final boolean hasFocus,
final int row,
final int column) {
final Component c = tableCellRenderer.getTableCellRendererComponent(table,
value, isSelected, hasFocus, row,
column);
if (c instanceof JLabel) {
final JLabel l = (JLabel) c;
l.setHorizontalTextPosition(JLabel.LEFT);
final int modelColumn = table.convertColumnIndexToModel(column);
l.setIcon(getHeaderRendererIcon(modelColumn, (int) (l.getFont().getSize() * 1.6)));
}
return c;
}
}
private Icon getHeaderRendererIcon(final int column, final int size) {
final Directive directive = getDirective(column);
if (directive == EMPTY_DIRECTIVE) {
return null;
}
return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive));
}
private int getSortingDirection(final int column) {
return getDirective(column).direction;
}
private void clearSortingDirections() {
sortingColumns.clear();
}
private Directive getDirective(final int column) {
for (Object sortingColumn : sortingColumns) {
final Directive directive = (Directive) sortingColumn;
if (directive.column == column) {
return directive;
}
}
return EMPTY_DIRECTIVE;
}
private static class Directive {
private final int column;
private final int direction;
public Directive(final int column, final int direction) {
this.column = column;
this.direction = direction;
}
}
private static class Arrow implements Icon {
private final boolean descending;
private final int size;
private final int priority;
public Arrow(final boolean descending, final int size, final int priority) {
this.descending = descending;
this.size = size;
this.priority = priority;
}
public void paintIcon(final Component c, final Graphics g, final int x, int y) {
final Color color = c == null ? Color.GRAY : c.getBackground();
// In a compound sort, make each successive triangle 20%
// smaller than the previous one.
final int dx = (int) (size / 2 * FastMath.pow(0.8, priority));
final int dy = descending ? dx : -dx;
// Align icon (roughly) with font baseline.
y = y + 5 * size / 6 + (descending ? -dy : 0);
final int shift = descending ? 1 : -1;
g.translate(x, y);
// Right diagonal.
g.setColor(color.darker());
g.drawLine(dx / 2, dy, 0, 0);
g.drawLine(dx / 2, dy + shift, 0, shift);
// Left diagonal.
g.setColor(color.brighter());
g.drawLine(dx / 2, dy, dx, 0);
g.drawLine(dx / 2, dy + shift, dx, shift);
// Horizontal line.
if (descending) {
g.setColor(color.darker().darker());
} else {
g.setColor(color.brighter().brighter());
}
g.drawLine(dx, 0, 0, 0);
g.setColor(color);
g.translate(-x, -y);
}
public int getIconWidth() {
return size;
}
public int getIconHeight() {
return size;
}
}
private class Row implements Comparable {
private final int modelIndex;
public Row(final int index) {
modelIndex = index;
}
public int compareTo(final Object o) {
final int idxRow1 = modelIndex;
final int idxRow2 = ((Row) o).modelIndex;
if (doSortBy) {
final Object o1 = tableModel.getValueAt(idxRow1, 0);
final Object o2 = tableModel.getValueAt(idxRow2, 0);
final int comparison = SORTEDBY_COMPARATOR.compare(o1, o2);
if (comparison != 0) {
return sortedByDirection == DESCENDING ? -comparison : comparison;
}
}
for (Object sortingColumn : sortingColumns) {
final Directive directive = (Directive) sortingColumn;
final int column = directive.column;
final Object o1 = tableModel.getValueAt(idxRow1, column);
final Object o2 = tableModel.getValueAt(idxRow2, column);
final int comparison = getComparator(column).compare(o1, o2);
if (comparison != 0) {
return directive.direction == DESCENDING ? -comparison : comparison;
}
}
return 0;
}
}
private static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
public int compare(final Object o1, final Object o2) {
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
return ((Comparable) o1).compareTo(o2);
}
}
};
private static final Comparator LEXICAL_COMPARATOR = new Comparator() {
public int compare(final Object o1, final Object o2) {
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
return o1.toString().compareTo(o2.toString());
}
}
};
private final Comparator SORTEDBY_COMPARATOR = new Comparator() {
public int compare(final Object o1, final Object o2) {
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else if (o1 instanceof ProductEntry && o2 instanceof ProductEntry) {
final ProductEntry e1 = (ProductEntry) o1;
final ProductEntry e2 = (ProductEntry) o2;
if (sortBy.equals(SORT_BY.NAME)) {
return e1.getName().compareTo(e2.getName());
} else if (sortBy.equals(SORT_BY.TYPE)) {
return e1.getProductType().compareTo(e2.getProductType());
} else if (sortBy.equals(SORT_BY.MISSON)) {
return e1.getMission().compareTo(e2.getMission());
} else if (sortBy.equals(SORT_BY.DATE)) {
return e1.getFirstLineTime().getAsDate().compareTo(e2.getFirstLineTime().getAsDate());
} else if (sortBy.equals(SORT_BY.FILESIZE)) {
final Long size1 = e1.getFileSize();
final Long size2 = e2.getFileSize();
return size1.compareTo(size2);
}
}
return o1.toString().compareTo(o2.toString());
}
};
private Comparator getComparator(final int column) {
final DataProvider dataProvider = tableModel.getDataProvider(column);
final Class columnType = tableModel.getColumnClass(column);
Comparator comparator = dataProvider.getComparator();
if (comparator == null) {
if (Comparable.class.isAssignableFrom(columnType)) {
comparator = COMPARABLE_COMAPRATOR;
} else {
comparator = LEXICAL_COMPARATOR;
}
}
return comparator;
}
}