/*
* Copyright 2007 - 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.jailer.ui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
/**
* Table showing collection progress.
*
* @author Ralf Wisser
*/
public class ProgressTable extends JTable {
/**
* Maximum number of tables in a closure-table's line.
*/
private final static int MAX_TABLES_PER_LINE = 7;
/**
* Background colors.
*/
private final List<Color> bgColors = new ArrayList<Color>();
private final Color BG1 = new Color(255, 255, 255);
private final Color BG2 = new Color(240, 255, 255);
private final Color SELECTED_FG = new Color(180, 160, 0);
private final Color INPROGRESS_FG = new Color(255, 40, 0);
/**
* Holds infos about a cell.
*/
public static class CellInfo {
// In
public String tableName;
public long numberOfRows;
public Set<String> parentNames;
// Calculated
public int row, column;
public List<CellInfo> parents;
public boolean inProgress = false;
};
/**
* Holds infos about a cells.
*/
private List<List<CellInfo>> cellInfos = new ArrayList<List<CellInfo>>();
/**
* Selected cells.
*/
private Set<CellInfo> selectedCells = new HashSet<CellInfo>();
private String selectedTableName = null;
/**
* Total number of collected rows.
*/
private long totalNumberOfCollectedRows = 0;
/**
* Cell render components.
*/
private final JPanel cellPanel = new JPanel();
private final JLabel tableRender = new JLabel("");
private final JLabel numberRender = new JLabel("");
/** Creates new table */
public ProgressTable() {
setShowGrid(false);
setSurrendersFocusOnKeystroke(true);
tableRender.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
numberRender.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
cellPanel.setLayout(new GridBagLayout());
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 1;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTH;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.weighty = 1.0;
cellPanel.add(tableRender, gridBagConstraints);
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 2;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.weighty = 1.0;
cellPanel.add(numberRender, gridBagConstraints);
tableRender.setOpaque(true);
numberRender.setOpaque(true);
setRowHeight(getRowHeight() * 3);
final Border defaultBorder = cellPanel.getBorder();
final Border selBorder = BorderFactory.createLineBorder(Color.BLACK, 2);
setDefaultRenderer(Object.class, new TableCellRenderer() {
private Font font = new JLabel("normal").getFont();
private Font normal = new Font(font.getName(), font.getStyle() & ~Font.BOLD, font.getSize());
private Font kursiv = new Font(font.getName(), (font.getStyle() & ~Font.BOLD) | Font.ITALIC, font.getSize());
private Font bold = new Font(font.getName(), font.getStyle() | Font.BOLD, font.getSize());
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Color color = bgColors.get(row);
boolean fontIsSet = false;
tableRender.setForeground(Color.BLACK);
numberRender.setForeground(Color.BLACK);
cellPanel.setToolTipText(null);
cellPanel.setBorder(defaultBorder);
if (value instanceof CellInfo) {
CellInfo cellInfo = (CellInfo) value;
if (cellInfo.tableName.equals(selectedTableName)) {
cellPanel.setBorder(selBorder);
}
tableRender.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
tableRender.setText(cellInfo.tableName);
cellPanel.setToolTipText(cellInfo.tableName);
if (selectedCells.contains(cellInfo)) {
numberRender.setFont(bold);
tableRender.setFont(bold);
fontIsSet = true;
}
if (cellInfo.numberOfRows < 0) {
numberRender.setText(" ");
tableRender.setForeground(cellInfo.inProgress? INPROGRESS_FG : SELECTED_FG);
numberRender.setForeground(cellInfo.inProgress? INPROGRESS_FG : SELECTED_FG);
} else if (cellInfo.numberOfRows == 0) {
numberRender.setText(cellInfo.numberOfRows + " rows");
tableRender.setForeground(Color.GRAY);
numberRender.setForeground(Color.GRAY);
} else {
long p = 0;
if (totalNumberOfCollectedRows > 0) {
p = (cellInfo.numberOfRows * 100) / totalNumberOfCollectedRows;
if (p > 100) { // should not happen
p = 100;
}
}
numberRender.setText(cellInfo.numberOfRows + " rows" + (p == 0 ? "" : " (" + p + "%)"));
tableRender.setForeground(Color.BLACK);
float f = 0.2f + (p / 100.0f) * 0.8f;
if (f > 1.0f) {
f = 1.0f;
}
if (p == 0) {
f = 0.0f;
}
numberRender.setForeground(new Color(f, 0.0f, 0.0f));
}
} else {
tableRender.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
tableRender.setText(" " + value);
numberRender.setText("");
}
if (!fontIsSet) {
numberRender.setFont(kursiv);
tableRender.setFont(normal);
}
tableRender.setBackground(color);
numberRender.setBackground(color);
cellPanel.setBackground(color);
return cellPanel;
}
});
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent evt) {
int col = getSelectedColumn();
int row = getSelectedRow();
if (col >= 1 && row >= 0) {
Object o = getModel().getValueAt(row, col);
if (o instanceof CellInfo) {
CellInfo cellInfo = (CellInfo) o;
selectedCells.clear();
selectedTableName = cellInfo.tableName;
multiSelection = false;
selectCell(cellInfo);
repaint();
getSelectionModel().clearSelection();
}
}
}
});
Object[] colNames = new String[MAX_TABLES_PER_LINE + 1];
for (int i = 0; i < colNames.length; ++i) {
colNames[i] = "";
}
tableModel = new DefaultTableModel(colNames, 0) {
public boolean isCellEditable(int row, int column) {
return false;
}
private static final long serialVersionUID = 772166119760096519L;
};
setModel(tableModel);
setIntercellSpacing(new Dimension(0, 0));
getColumnModel().getColumns().nextElement().setMaxWidth(50);
setTableHeader(null);
}
private boolean multiSelection = false;
/**
* Recursively selects cells.
*
* @param cellInfo current cell
*/
private void selectCell(CellInfo cellInfo) {
selectedCells.add(cellInfo);
if (cellInfo.parents != null) {
for (CellInfo p : cellInfo.parents) {
selectCell(p);
}
}
}
/**
* Selects all cells which render a given table.
*
* @param tableName name of the table
*/
public void selectAllCells(String tableName) {
multiSelection = true;
selectedCells.clear();
selectedTableName = tableName;
for (List<CellInfo> cL : cellInfos) {
for (CellInfo cellInfo : cL) {
if (cellInfo != null) {
if (cellInfo.tableName.equals(tableName)) {
if (cellInfo.numberOfRows > 0) {
selectCell(cellInfo);
}
}
}
}
}
repaint();
}
private DefaultTableModel tableModel;
/**
* Paints the links to parent tables.
*/
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
if (!(graphics instanceof Graphics2D))
return;
Graphics2D g2d = (Graphics2D) graphics;
Color color = new Color(0, 80, 255, 80);
Color selColor = new Color(255, 0, 0, 120);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int[] x = new int[2];
int[] y = new int[2];
List<int[]> hLineCount = new ArrayList<int[]>();
for (List<CellInfo> cL : cellInfos) {
for (CellInfo cellInfo : cL) {
g2d.setColor(selectedCells.contains(cellInfo) ? selColor : color);
g2d.setStroke(new BasicStroke(selectedCells.contains(cellInfo) ? 3 : 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
while (cellInfo != null && hLineCount.size() <= cellInfo.row) {
hLineCount.add(new int[MAX_TABLES_PER_LINE]);
}
if (cellInfo != null && cellInfo.parents != null) {
for (CellInfo parent : cellInfo.parents) {
int offset = 0;
if (cellInfo.column == parent.column) {
offset = 0;
for (int r = parent.row; r < cellInfo.row; ++r) {
if (offset < hLineCount.get(r)[cellInfo.column]) {
offset = hLineCount.get(r)[cellInfo.column];
}
++hLineCount.get(r)[cellInfo.column];
}
offset = ((offset + 1) / 2) * (2 * (offset % 2) - 1) * 6;
}
Rectangle r = getCellRect(cellInfo.row, cellInfo.column + 1, false);
x[0] = (int) r.getCenterX();
y[0] = (int) r.getMinY() + r.height / 5;
r = getCellRect(parent.row, parent.column + 1, false);
x[1] = (int) r.getCenterX();
y[1] = (int) r.getMaxY() - r.height / 4;
drawArrow(g2d, x[1] + offset, y[1], x[0] + offset, y[0]);
}
}
}
}
}
/**
* Returns visibility of last row of the table.
*
* @return <code>true</code> iff last row of the table is visible
*/
public boolean isLastRowVisible() {
if (getRowCount() == 0) {
return false;
}
Rectangle r = getCellRect(getRowCount() - 1, 1, true);
Rectangle visible = getVisibleRect();
boolean isVisible = r.y <= visible.y + visible.height;
return isVisible;
// return visible.y <= r.y && visible.y + visible.height >= r.y + r.height;
}
/**
* Removes last row and adds a new one.
*
* @param cells the row to add
* @param day the day
*/
public void replaceLastRow(List<CellInfo> cells, int day) {
boolean scrollToBottom = isLastRowVisible();
boolean checkSelection = false;
if (selectedTableName != null) {
for (CellInfo cellInfo : cellInfos.get(cellInfos.size() - 1)) {
if (cellInfo != null && selectedTableName.equals(cellInfo.tableName) && selectedCells.contains(cellInfo)) {
selectCell(cellInfo);
checkSelection = true;
break;
}
}
}
if (cellInfos.size() > 0) {
Set<Integer> rowsToRemove = new TreeSet<Integer>();
for (CellInfo ci : cellInfos.remove(cellInfos.size() - 1)) {
rowsToRemove.add(ci.row);
}
for (int rowI = rowsToRemove.size(); rowI > 0; --rowI) {
tableModel.removeRow(tableModel.getRowCount() - 1);
bgColors.remove(bgColors.size() - 1);
}
}
addRow(cells, day, scrollToBottom);
if (selectedTableName != null && (checkSelection || multiSelection)) {
if (multiSelection) {
selectAllCells(selectedTableName);
} else {
boolean f = false;
for (CellInfo cellInfo : cellInfos.get(cellInfos.size() - 1)) {
if (cellInfo != null && selectedTableName.equals(cellInfo.tableName)) {
selectCell(cellInfo);
f = true;
break;
}
}
if (!f) {
selectedCells.clear();
selectedTableName = null;
}
}
}
}
/**
* Adds a row of {@link CellInfo}s to the table.
*
* @param cells the row to add
* @param day the day
*/
public void addRow(List<CellInfo> cells, int day) {
addRow(cells, day, isLastRowVisible());
}
/**
* Caches optimization result.
*/
private List<CellInfo> lastBestRow = null;
/**
* Adds a row of {@link CellInfo}s to the table.
*
* @param cells the row to add
* @param day the day
* @param scrollToBottom if <code>true</code>, scroll table to bottom row
*/
public void addRow(List<CellInfo> cells, int day, boolean scrollToBottom) {
List<CellInfo> row = new LinkedList<CellInfo>(cells);
int leftPad = (MAX_TABLES_PER_LINE - cells.size()) / 2;
while (leftPad-- > 0) {
row.add(0, null);
}
int s = (row.size() * 3) / 2;
while (row.size() < s) {
row.add(null);
}
while (row.size() % MAX_TABLES_PER_LINE != 0) {
row.add(null);
}
String dayS = "" + day;
List<CellInfo> parentRow = null;
for (int i = cellInfos.size() - 1; i > 0; --i) {
if (cellInfos.get(i).isEmpty()) {
dayS = i + ", " + dayS;
} else {
parentRow = cellInfos.get(i);
break;
}
}
for (CellInfo cellInfo : row) {
if (cellInfo != null && cellInfo.parentNames != null && parentRow != null) {
cellInfo.parents = new ArrayList<CellInfo>();
for (String pName : cellInfo.parentNames) {
for (CellInfo pInfo : parentRow) {
if (pName.equals(pInfo.tableName)) {
cellInfo.parents.add(pInfo);
break;
}
}
}
}
}
boolean takeLastOrdering = false;
if (lastBestRow != null) {
takeLastOrdering = true;
Set<String> lastNames = new HashSet<String>();
for (CellInfo ci: lastBestRow) {
if (ci != null) {
lastNames.add(ci.tableName);
}
}
Set<String> currentNames = new HashSet<String>();
for (CellInfo ci: row) {
if (ci != null) {
currentNames.add(ci.tableName);
}
}
if (!currentNames.equals(lastNames)) {
takeLastOrdering = false;
} else {
for (CellInfo ci: lastBestRow) {
if (ci != null) {
CellInfo cie = null;
for (CellInfo ciRow: row) {
if (ciRow != null && ciRow.tableName.equals(ci.tableName)) {
cie = ciRow;
break;
}
}
if (cie == null) {
takeLastOrdering = false;
break;
}
if (cie.parentNames == null && ci.parentNames != null || cie.parentNames != null && ci.parentNames == null) {
takeLastOrdering = false;
break;
}
if (cie.parentNames != null && !cie.parentNames.equals(ci.parentNames)) {
takeLastOrdering = false;
break;
}
}
}
}
}
if (takeLastOrdering) {
List<CellInfo> newRow = new ArrayList<CellInfo>();
for (CellInfo ci: lastBestRow) {
CellInfo cie = null;
if (ci != null) {
for (CellInfo ciRow: row) {
if (ciRow != null && ciRow.tableName.equals(ci.tableName)) {
cie = ciRow;
break;
}
}
}
newRow.add(cie);
}
row = newRow;
} else {
int numUnknown = 0;
int all = 0;
for (CellInfo ci: row) {
if (ci != null) {
++all;
if (ci.numberOfRows < 0) {
++numUnknown;
}
}
}
int cd = 120000;
int reducedTimeQuot = 1;
if (numUnknown > 0.7 * all) {
reducedTimeQuot = 2;
cd /= reducedTimeQuot;
}
if (row.size() > 0) {
cd = cd * 50 / row.size();
}
int maxParentRow = 1;
if (parentRow != null) {
for (CellInfo i: parentRow) {
if (i != null && i.row > maxParentRow) {
maxParentRow = i.row;
}
}
}
List<CellInfo> bestRow;
double fitness = fitness(row, maxParentRow);
bestRow = new ArrayList<CellInfo>(row);
long startTime = System.currentTimeMillis();
do {
for (int a = row.size() - 1; a >= 0; --a) {
for (int b = a - 1; b >= 0; --b) {
CellInfo cA = row.get(a);
CellInfo cB = row.get(b);
row.set(a, cB);
row.set(b, cA);
double f = fitness(row, maxParentRow);
if (f < fitness) {
bestRow = new ArrayList<CellInfo>(row);
fitness = f;
}
row.set(a, cA);
row.set(b, cB);
--cd;
if (cd < 0) {
break;
}
if (cd % 100 == 0) {
if (System.currentTimeMillis() - startTime > (500 / reducedTimeQuot)) {
cd = -1;
break;
}
}
}
if (cd < 0) {
break;
}
}
if (bestRow != null) {
row = bestRow;
}
} while (bestRow != null && cd > 0);
lastBestRow = new ArrayList<CellInfo>(row);
}
dayS = " " + dayS;
cellInfos.add(cells);
int y = tableModel.getRowCount();
int l = 0;
for (List<CellInfo> cl : cellInfos) {
if (!cl.isEmpty()) {
++l;
}
}
Color bg = (l % 2 == 0) ? BG1 : BG2;
while (!row.isEmpty()) {
Object[] rowO = new Object[MAX_TABLES_PER_LINE + 1];
boolean rowIsEmpty = true;
rowO[0] = dayS;
dayS = "";
for (int x = 0; x < MAX_TABLES_PER_LINE; ++x) {
CellInfo cellInfo = row.isEmpty() ? null : row.remove(0);
if (cellInfo == null) {
rowO[x + 1] = "";
} else {
rowIsEmpty = false;
cellInfo.column = x;
cellInfo.row = y;
rowO[x + 1] = cellInfo;
}
}
if (!rowIsEmpty) {
bgColors.add(bg);
tableModel.addRow(rowO);
++y;
}
}
if (scrollToBottom && getRowCount() > 0) {
Rectangle cellRect = getCellRect(getRowCount() - 1, 1, true);
scrollRectToVisible(cellRect);
scrollRectToVisible(cellRect);
}
invalidate();
repaint();
}
/**
* Fitness (qualitiy) of the ordering of a row.
*
* @param row the row
* @return the fitness, less is better
*/
private double fitness(List<CellInfo> row, int maxParentRow) {
double f = 0;
int conflictCount[] = new int[MAX_TABLES_PER_LINE];
for (int x = row.size() - 1; x >= 0; --x) {
CellInfo cellInfo = row.get(x);
// if (cellInfo != null) {
// f += (x / MAX_TABLES_PER_LINE) / 2.0;
// }
if (cellInfo != null && cellInfo.parents != null) {
for (CellInfo parent : cellInfo.parents) {
double xabs = (x % MAX_TABLES_PER_LINE) - parent.column;
if (xabs == 0.0) {
f += MAX_TABLES_PER_LINE * MAX_TABLES_PER_LINE * conflictCount[parent.column]++;
}
double yabs = (x / MAX_TABLES_PER_LINE) + maxParentRow - parent.row;
f += xabs * xabs + yabs * yabs / 10;
}
}
}
return f;
}
/**
* Sets total number of collected rows.
*
* @param totalNumberOfCollectedRows
* total number of collected rows
*/
public void setTotalNumberOfCollectedRows(long totalNumberOfCollectedRows) {
this.totalNumberOfCollectedRows = totalNumberOfCollectedRows;
}
/**
* Draws an arrow on the given Graphics2D context
*
* @param g
* The Graphics2D context to draw on
* @param x
* The x location of the "tail" of the arrow
* @param y
* The y location of the "tail" of the arrow
* @param xx
* The x location of the "head" of the arrow
* @param yy
* The y location of the "head" of the arrow
*/
private void drawArrow(Graphics2D g, int x, int y, int xx, int yy) {
float arrowWidth = 6.0f;
float theta = 0.423f;
int[] xPoints = new int[3];
int[] yPoints = new int[3];
float[] vecLine = new float[2];
float[] vecLeft = new float[2];
float fLength;
float th;
float ta;
float baseX, baseY;
xPoints[0] = xx;
yPoints[0] = yy;
// build the line vector
vecLine[0] = (float) xPoints[0] - x;
vecLine[1] = (float) yPoints[0] - y;
// build the arrow base vector - normal to the line
vecLeft[0] = -vecLine[1];
vecLeft[1] = vecLine[0];
// setup length parameters
fLength = (float) Math.sqrt(vecLine[0] * vecLine[0] + vecLine[1] * vecLine[1]);
th = arrowWidth / (2.0f * fLength);
ta = arrowWidth / (2.0f * ((float) Math.tan(theta) / 2.0f) * fLength);
// find the base of the arrow
baseX = ((float) xPoints[0] - ta * vecLine[0]);
baseY = ((float) yPoints[0] - ta * vecLine[1]);
// build the points on the sides of the arrow
xPoints[1] = (int) (baseX + th * vecLeft[0]);
yPoints[1] = (int) (baseY + th * vecLeft[1]);
xPoints[2] = (int) (baseX - th * vecLeft[0]);
yPoints[2] = (int) (baseY - th * vecLeft[1]);
g.drawLine(x, y, (int) baseX, (int) baseY);
g.fillPolygon(xPoints, yPoints, 3);
}
/**
* Adjusts with of table columns.
*/
public void adjustColumnWidth() {
if (getRowCount() == 0) {
return;
}
DefaultTableColumnModel colModel = (DefaultTableColumnModel) getColumnModel();
for (int vColIndex = 1; vColIndex < colModel.getColumnCount(); ++vColIndex) {
TableColumn col = colModel.getColumn(vColIndex);
boolean isEmpty = true;
for (int r = 0; r < getRowCount(); r++) {
TableCellRenderer renderer = getCellRenderer(r, vColIndex);
Component comp = renderer.getTableCellRendererComponent(this, getValueAt(r, vColIndex), false, false, r, vColIndex);
if (comp != cellPanel || tableRender.getText().trim().length() > 0) {
isEmpty = false;
break;
}
}
int w;
if (isEmpty) {
w = 1;
} else {
w = 10000;
}
col.setMaxWidth(w);
}
}
private static final long serialVersionUID = -6284876860992859979L;
}