/*
* jMemorize - Learning made easy (and fun) - A Leitner flashcards tool
* Copyright(C) 2004-2008 Riad Djemili and contributors
*
* 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 1, 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, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package jmemorize.gui.swing.panels;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import jmemorize.core.Card;
import jmemorize.core.Category;
import jmemorize.core.CategoryObserver;
import jmemorize.gui.LC;
import jmemorize.gui.Localization;
import jmemorize.gui.swing.ColorConstants;
import jmemorize.gui.swing.frames.MainFrame;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.TickUnitSource;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.CategoryItemEntity;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRendererState;
import org.jfree.chart.renderer.category.StackedBarRenderer3D;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.TextAnchor;
/**
* This is the panel that is being displayed up the upper part of jMemorize all
* of the time. It shows a visual representation of the current card
* distribution among all decks in form of a 3D stacked bar chart.
*
* @author djemili
*/
public class DeckChartPanel extends JPanel implements CategoryObserver
{
/**
* A mouse listener for clicks on the chart. If a bar is clicked the view
* changes to the selected deck.
*/
private class MouseClicked implements ChartMouseListener
{
/* (non-Javadoc)
* @see org.jfree.chart.ChartMouseListener
*/
public void chartMouseClicked(ChartMouseEvent evt)
{
ChartEntity entity = evt.getEntity();
if (entity instanceof CategoryItemEntity)
{
int cat = ((CategoryItemEntity)entity).getCategoryIndex();
m_frame.setDeck(cat - 1);
}
}
/* (non-Javadoc)
* @see org.jfree.chart.ChartMouseListener
*/
public void chartMouseMoved(ChartMouseEvent arg0)
{
// do nothing
}
}
private class MyBarRenderer extends StackedBarRenderer3D
{
private int m_deck = -2; // HACK
private Font m_defaultFont;
private Font m_boldFont;
MyBarRenderer()
{
m_defaultFont = getBaseItemLabelFont();
m_boldFont = getBaseItemLabelFont().deriveFont(Font.BOLD);
}
public void drawItem(Graphics2D g, CategoryItemRendererState state,
Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
ValueAxis rangeAxis, CategoryDataset data, int row, int column, int pass)
{
if (column - 1 == m_deck && m_category.getCards(m_deck).size() > 0)
{
setOutlinePaint(ColorConstants.SELECTION_COLOR, false);
setBaseItemLabelFont(m_boldFont, false);
setItemLabelFont(m_boldFont, false);
}
else
{
setOutlinePaint(Color.WHITE, false);
setBaseItemLabelFont(m_defaultFont, false);
setItemLabelFont(m_defaultFont, false);
}
// domainAxis.setCategoryMargin(0.2 + (0.011 * getMinNumDecks()));
super.drawItem(g, state, dataArea, plot, domainAxis, rangeAxis,
data, row, column, pass);
}
public void setSelectedDeck(int level)
{
m_deck = level;
notifyListeners(new RendererChangeEvent(this));
}
/*
* NOTE this is a workaround. JFreeChart appears to insist on drawing
* bars even if they are of zero width. To prevent the top of the
* stacked bar chart from being the wrong color, we need to skip bars
* that are zero width. Here, we intercept the draw call to remove any
* zero width blocks in the stack before drawing
*/
protected void drawStackVertical(List values, Comparable category,
Graphics2D g2, CategoryItemRendererState state, Rectangle2D dataArea,
CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis,
CategoryDataset dataset)
{
List prunedValues = new ArrayList(values);
double lastValue = 0.0;
Iterator it = prunedValues.iterator();
while (it.hasNext())
{
Object[] pair = (Object[]) it.next();
double thisValue = ((Double)pair[1]).doubleValue();
double delta = thisValue - lastValue;
if( pair[0] != null)
{
if (delta == 0.0)
it.remove();
}
lastValue = thisValue;
}
super.drawStackVertical(prunedValues, category, g2, state, dataArea,
plot, domainAxis, rangeAxis, dataset);
}
}
// TODO make minimum deck bars dependent on screen resolution
private final static String DECK0_NAME =
Localization.get("DeckChart.START_DECK"); //$NON-NLS-1$
private final static String SUMMARY_BAR_NAME =
Localization.get("DeckChart.SUMMARY"); //$NON-NLS-1$
private final static String LEARNED_CARDS_ROW =
Localization.get("DeckChart.LEARNED_CARDS");
private final static String EXPIRED_CARDS_ROW =
Localization.get("DeckChart.EXPIRED_CARDS");
private final static String UNLEARNED_CARDS_ROW =
Localization.get("DeckChart.UNLEARNED_CARDS");
private Category m_category;
private MainFrame m_frame;
private DefaultCategoryDataset m_dataset;
private ChartPanel m_chartPanel;
private MyBarRenderer m_barRenderer;
public DeckChartPanel(MainFrame mainFrame)
{
m_frame = mainFrame;
initComponents();
}
public void setCategory(Category category)
{
if (m_category != null)
{
m_category.removeObserver(this);
}
m_category = category;
category.addObserver(this);
createDataset();
}
public void setDeck(int level)
{
m_barRenderer.setSelectedDeck(level);
}
/* (non-Javadoc)
* @see jmemorize.core.CategoryObserver
*/
public void onCategoryEvent(int type, Category category)
{
// ignore. mainframe already looks for important category changes
}
/* (non-Javadoc)
* @see jmemorize.core.CategoryObserver
*/
public void onCardEvent(int type, Card card, Category category, int level)
{
updateBars();
}
private JFreeChart createChart()
{
m_dataset = createDefaultDataSet();
JFreeChart chart = ChartFactory.createStackedBarChart3D(
null, // chart title
null, // domain axis label
Localization.get("DeckChart.CARDS"), // range axis label //$NON-NLS-1$
m_dataset, // data
PlotOrientation.VERTICAL, // the plot orientation
true, // include legend
true, // tooltips
false // urls
);
// setup legend
// TODO we used to do this for the old jfreechar version, but it's not clear why.
// can we get rid of it?
LegendTitle legend = chart.getLegend();
// legend.setsetRenderingOrder(LegendRenderingOrder.REVERSE);
legend.setItemFont(legend.getItemFont().deriveFont(11f));
// setup plot
CategoryPlot plot = (CategoryPlot)chart.getPlot();
TickUnitSource tickUnits = NumberAxis.createIntegerTickUnits();
plot.getRangeAxis().setStandardTickUnits(tickUnits); //CHECK use locale
plot.setForegroundAlpha(0.99f);
// setup renderer
m_barRenderer = new MyBarRenderer();
m_barRenderer.setItemLabelGenerator(new StandardCategoryItemLabelGenerator());
m_barRenderer.setItemLabelsVisible(true);
m_barRenderer.setPositiveItemLabelPosition(
new ItemLabelPosition(ItemLabelAnchor.CENTER, TextAnchor.CENTER)
);
plot.setRenderer(m_barRenderer);
setSeriesPaint();
return chart;
}
private void createDataset()
{
m_dataset = createDefaultDataSet();
updateBars();
CategoryPlot plot = (CategoryPlot)m_chartPanel.getChart().getPlot();
plot.setDataset(m_dataset);
}
private void initComponents()
{
// add the chart to a panel...
m_chartPanel = new ChartPanel(createChart());
m_chartPanel.addChartMouseListener(new MouseClicked());
m_chartPanel.setMinimumDrawHeight(100);
m_chartPanel.setMinimumDrawWidth(400);
m_chartPanel.setMaximumDrawHeight(1600);
m_chartPanel.setMaximumDrawWidth(10000);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(10, 2, 2, 2));
add(m_chartPanel);
addComponentListener(new ComponentAdapter(){
public void componentResized(ComponentEvent e)
{
if (m_category != null)
updateBars();
}
});
}
private DefaultCategoryDataset createDefaultDataSet()
{
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
setValues(dataset, SUMMARY_BAR_NAME, 0, 0, 0);
for (int i = 0; i < getMinNumDecks(); i++)
{
setValues(dataset, getDeckLabel(i), 0, 0, 0);
}
return dataset;
}
private void updateBars() //CHECK put Dataset as argument!?
{
updateSummaryBar();
while (m_dataset.getColumnCount() > getNumDecks())
{
m_dataset.removeColumn(m_dataset.getColumnCount() - 1);
}
for (int i=0; i < getNumDecks() - 1; i++)
{
updateBar(i);
}
}
private void updateSummaryBar()
{
int learned = m_category.getLearnedCards().size();
int expired = m_category.getExpiredCards().size();
int unlearned = m_category.getUnlearnedCards().size();
setValues(m_dataset, SUMMARY_BAR_NAME, unlearned, expired, learned);
}
private void updateBar(int level)
{
if (level == 0)
{
int unlearnedCards = m_category.getCards(level).size();
setValues(m_dataset, DECK0_NAME, unlearnedCards, 0, 0);
}
else
{
String deckLabel = getDeckLabel(level);
if (level >= m_category.getNumberOfDecks())
{
setValues(m_dataset, deckLabel, 0, 0, 0);
}
else
{
int learnedCards = m_category.getLearnedCards(level).size();
int expiredCards = m_category.getExpiredCards(level).size();
setValues(m_dataset, deckLabel, 0, expiredCards, learnedCards);
}
}
}
/**
* Sets the values for the column with given id. This method also handles
* the order in which the rows will appear in the column.
*/
private void setValues(DefaultCategoryDataset dataset, String column,
int unlearned, int expired, int learned)
{
// if you change the order of the rows, don't forget to also update the
// method setSeriesPaint
dataset.setValue(unlearned, UNLEARNED_CARDS_ROW, column);
dataset.setValue(expired, EXPIRED_CARDS_ROW, column);
dataset.setValue(learned, LEARNED_CARDS_ROW, column);
}
private String getDeckLabel(int level)
{
return (level == 0) ? DECK0_NAME :
Localization.get(LC.DECK) + " " + level; //$NON-NLS-1$
}
private int getNumDecks()
{
return Math.max(m_category.getNumberOfDecks(), getMinNumDecks()) + 1;
}
/**
* The minimal amount of deck bars show at all time exclusive the summary
* bar.
*/
private int getMinNumDecks()
{
int width = getSize().width - 250;
return width / 135;
}
private void setSeriesPaint()
{
m_barRenderer.setSeriesPaint(0, ColorConstants.UNLEARNED_CARDS);
m_barRenderer.setSeriesPaint(1, ColorConstants.EXPIRED_CARDS);
m_barRenderer.setSeriesPaint(2, ColorConstants.LEARNED_CARDS);
}
}