org.yccheok.jstock.gui.charting.InvestmentFlowLayerUI.java Source code

Java tutorial

Introduction

Here is the source code for org.yccheok.jstock.gui.charting.InvestmentFlowLayerUI.java

Source

/*
 * JStock - Free Stock Market Software
 * Copyright (C) 2011 Yan Cheng CHEOK <yccheok@yahoo.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 2 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.yccheok.jstock.gui.charting;

import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.SwingUtilities;
import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.AbstractLayerUI;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.ui.RectangleEdge;
import org.yccheok.jstock.engine.Code;
import org.yccheok.jstock.engine.StockInfo;
import org.yccheok.jstock.gui.JStockOptions;
import org.yccheok.jstock.gui.JStock;
import org.yccheok.jstock.gui.Utils;
import org.yccheok.jstock.internationalization.GUIBundle;
import org.yccheok.jstock.internationalization.MessagesBundle;
import org.yccheok.jstock.portfolio.Activities;
import org.yccheok.jstock.portfolio.Activity;
import org.yccheok.jstock.portfolio.DecimalPlace;

/**
 *
 * @author yccheok
 */
public class InvestmentFlowLayerUI<V extends javax.swing.JComponent> extends AbstractLayerUI<V> {

    private enum Type {
        Invest, ROI
    }

    // Refactoring required here. Perhaps we should wrap up
    // (investPoint, investPointIndex) and (ROIPoint, ROIPointIndex) as class.
    private Point2D investPoint = null;
    private int investPointIndex = -1;
    private Point2D ROIPoint = null;
    private int ROIPointIndex = -1;

    private final Rectangle2D drawArea = new Rectangle();

    private final InvestmentFlowChartJDialog investmentFlowChartJDialog;

    private static final Color COLOR_BLUE = new Color(85, 85, 255);
    private static final Color COLOR_RED = new Color(255, 85, 85);
    private static final Color COLOR_RED_BORDER = new Color(255, 85, 85);
    private static final Color COLOR_RED_BACKGROUND = new Color(255, 255, 153);
    private static final Color COLOR_BLUE_BORDER = new Color(85, 85, 255);
    private static final Color COLOR_BLUE_BACKGROUND = new Color(255, 255, 153);
    private static final Color COLOR_BACKGROUND = new Color(255, 255, 153);
    private static final Color COLOR_BORDER = new Color(255, 204, 0);

    /* Will be updated by updateROIInformationBox. */
    private final List<String> ROIValues = new ArrayList<String>();
    private final List<String> ROIParams = new ArrayList<String>();
    private final Rectangle2D ROIRect = new Rectangle();
    private double totalROIValue = 0.0;

    /* Will be updated by updateInvestInformationBox. */
    private final List<String> investValues = new ArrayList<String>();
    private final List<String> investParams = new ArrayList<String>();
    private final Rectangle2D investRect = new Rectangle();
    private double totalInvestValue = 0.0;

    public InvestmentFlowLayerUI(InvestmentFlowChartJDialog cashFlowChartJDialog) {
        this.investmentFlowChartJDialog = cashFlowChartJDialog;
    }

    private void drawTitle(Graphics2D g2) {
        final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
        final Color oldColor = g2.getColor();
        final Font oldFont = g2.getFont();

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        final Font titleFont = oldFont.deriveFont(oldFont.getStyle() | Font.BOLD, (float) oldFont.getSize() * 1.5f);

        final int margin = 5;

        final FontMetrics titleFontMetrics = g2.getFontMetrics(titleFont);
        final FontMetrics oldFontMetrics = g2.getFontMetrics(oldFont);
        final java.text.NumberFormat numberFormat = java.text.NumberFormat.getInstance();
        numberFormat.setMaximumFractionDigits(2);
        numberFormat.setMinimumFractionDigits(2);

        final double totalInvestValue = this.investmentFlowChartJDialog.getTotalInvestValue();
        final double totalROIValue = this.investmentFlowChartJDialog.getTotalROIValue();

        final DecimalPlace decimalPlace = JStock.instance().getJStockOptions().getDecimalPlace();

        final String invest = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace,
                totalInvestValue);
        final String roi = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, totalROIValue);
        final double gain = totalROIValue - totalInvestValue;
        final double percentage = totalInvestValue > 0.0 ? gain / totalInvestValue * 100.0 : 0.0;
        final String gain_str = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, gain);
        final String percentage_str = numberFormat.format(percentage);

        final String SELECTED = this.investmentFlowChartJDialog.getCurrentSelectedString();
        final String INVEST = GUIBundle.getString("InvestmentFlowLayerUI_Invest");
        final String RETURN = GUIBundle.getString("InvestmentFlowLayerUI_Return");
        final String GAIN = (SELECTED.length() > 0 ? SELECTED + " " : "")
                + GUIBundle.getString("InvestmentFlowLayerUI_Gain");
        final String LOSS = (SELECTED.length() > 0 ? SELECTED + " " : "")
                + GUIBundle.getString("InvestmentFlowLayerUI_Loss");

        final int string_width = oldFontMetrics.stringWidth(INVEST + ": ")
                + titleFontMetrics.stringWidth(invest + " ") + oldFontMetrics.stringWidth(RETURN + ": ")
                + titleFontMetrics.stringWidth(roi + " ")
                + oldFontMetrics.stringWidth((gain >= 0 ? GAIN : LOSS) + ": ")
                + titleFontMetrics.stringWidth(gain_str + " (" + percentage_str + "%)");

        int x = (int) (this.investmentFlowChartJDialog.getChartPanel().getWidth() - string_width) >> 1;
        final int y = margin + titleFontMetrics.getAscent();

        g2.setFont(oldFont);
        g2.drawString(INVEST + ": ", x, y);
        x += oldFontMetrics.stringWidth(INVEST + ": ");
        g2.setFont(titleFont);
        g2.drawString(invest + " ", x, y);
        x += titleFontMetrics.stringWidth(invest + " ");
        g2.setFont(oldFont);
        g2.drawString(RETURN + ": ", x, y);
        x += oldFontMetrics.stringWidth(RETURN + ": ");
        g2.setFont(titleFont);
        g2.drawString(roi + " ", x, y);
        x += titleFontMetrics.stringWidth(roi + " ");
        g2.setFont(oldFont);
        if (gain >= 0) {
            if (gain > 0) {
                if (org.yccheok.jstock.engine.Utils.isFallBelowAndRiseAboveColorReverse()) {
                    g2.setColor(JStock.instance().getJStockOptions().getLowerNumericalValueForegroundColor());
                } else {
                    g2.setColor(JStock.instance().getJStockOptions().getHigherNumericalValueForegroundColor());
                }
            }
            g2.drawString(GAIN + ": ", x, y);
            x += oldFontMetrics.stringWidth(GAIN + ": ");
        } else {
            if (org.yccheok.jstock.engine.Utils.isFallBelowAndRiseAboveColorReverse()) {
                g2.setColor(JStock.instance().getJStockOptions().getHigherNumericalValueForegroundColor());
            } else {
                g2.setColor(JStock.instance().getJStockOptions().getLowerNumericalValueForegroundColor());
            }
            g2.drawString(LOSS + ": ", x, y);
            x += oldFontMetrics.stringWidth(LOSS + ": ");
        }
        g2.setFont(titleFont);
        g2.drawString(gain_str + " (" + percentage_str + "%)", x, y);

        g2.setColor(oldColor);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
        g2.setFont(oldFont);
    }

    private void drawInformationBox(Graphics2D g2, Activities activities, Rectangle2D rect, List<String> params,
            List<String> values, String totalParam, double totalValue, Color background_color, Color border_color) {
        final Font oldFont = g2.getFont();
        final Font paramFont = oldFont;
        final FontMetrics paramFontMetrics = g2.getFontMetrics(paramFont);
        final Font valueFont = oldFont.deriveFont(oldFont.getStyle() | Font.BOLD, (float) oldFont.getSize() + 1);
        final FontMetrics valueFontMetrics = g2.getFontMetrics(valueFont);
        final Font dateFont = oldFont.deriveFont((float) oldFont.getSize() - 1);
        final FontMetrics dateFontMetrics = g2.getFontMetrics(dateFont);

        final int x = (int) rect.getX();
        final int y = (int) rect.getY();
        final int width = (int) rect.getWidth();
        final int height = (int) rect.getHeight();

        final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
        final Composite oldComposite = g2.getComposite();
        final Color oldColor = g2.getColor();

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(border_color);
        g2.drawRoundRect(x - 1, y - 1, width + 1, height + 1, 15, 15);
        g2.setColor(background_color);
        g2.setComposite(Utils.makeComposite(0.75f));
        g2.fillRoundRect(x, y, width, height, 15, 15);
        g2.setComposite(oldComposite);
        g2.setColor(oldColor);

        final Date date = activities.getDate().getTime();
        final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE, MMMM d, yyyy");
        final String dateString = simpleDateFormat.format(date);

        final int padding = 5;

        int yy = y + padding + dateFontMetrics.getAscent();
        g2.setFont(dateFont);
        g2.setColor(COLOR_BLUE);
        g2.drawString(dateString, ((width - dateFontMetrics.stringWidth(dateString)) >> 1) + x, yy);

        int index = 0;
        final int dateInfoHeightMargin = 5;
        final int infoTotalHeightMargin = 5;
        final int paramValueHeightMargin = 0;

        yy += dateFontMetrics.getDescent() + dateInfoHeightMargin
                + Math.max(paramFontMetrics.getAscent(), valueFontMetrics.getAscent());
        for (String param : params) {
            final String value = values.get(index++);
            g2.setColor(Color.BLACK);
            g2.setFont(paramFont);
            g2.drawString(param + ":", padding + x, yy);
            g2.setFont(valueFont);
            g2.drawString(value, width - padding - valueFontMetrics.stringWidth(value) + x, yy);
            // Same as yy += valueFontMetrics.getDescent() + paramValueHeightMargin + valueFontMetrics.getAscent()
            yy += paramValueHeightMargin + Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight());
        }

        if (values.size() > 1) {
            yy -= paramValueHeightMargin;
            yy += infoTotalHeightMargin;
            if (totalValue > 0.0) {
                g2.setColor(JStockOptions.DEFAULT_HIGHER_NUMERICAL_VALUE_FOREGROUND_COLOR);
            } else if (totalValue < 0.0) {
                g2.setColor(JStockOptions.DEFAULT_LOWER_NUMERICAL_VALUE_FOREGROUND_COLOR);
            }

            g2.setFont(paramFont);
            g2.drawString(totalParam + ":", padding + x, yy);
            g2.setFont(valueFont);
            final DecimalPlace decimalPlace = JStock.instance().getJStockOptions().getDecimalPlace();
            final String totalValueStr = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace,
                    totalValue);
            g2.drawString(totalValueStr, width - padding - valueFontMetrics.stringWidth(totalValueStr) + x, yy);
        }

        g2.setColor(oldColor);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
        g2.setFont(oldFont);
    }

    private double convertToPoundIfNecessary(Code code, double value) {
        final boolean shouldConvertPenceToPound = org.yccheok.jstock.portfolio.Utils
                .shouldConvertPenceToPound(this.investmentFlowChartJDialog.getPortfolioRealTimeInfo(), code);

        if (shouldConvertPenceToPound == false) {
            return value;
        }
        return value / 100.0;
    }

    private void updateROIInformationBox(Graphics2D g2) {
        final Font oldFont = g2.getFont();
        final Font paramFont = oldFont;
        final FontMetrics paramFontMetrics = g2.getFontMetrics(paramFont);
        final Font valueFont = oldFont.deriveFont(oldFont.getStyle() | Font.BOLD, (float) oldFont.getSize() + 1);
        final FontMetrics valueFontMetrics = g2.getFontMetrics(valueFont);
        final Font dateFont = oldFont.deriveFont((float) oldFont.getSize() - 1);
        final FontMetrics dateFontMetrics = g2.getFontMetrics(dateFont);

        final Activities activities = this.investmentFlowChartJDialog.getROIActivities(this.ROIPointIndex);

        this.ROIValues.clear();
        this.ROIParams.clear();
        this.totalROIValue = 0.0;

        final DecimalPlace decimalPlace = JStock.instance().getJStockOptions().getDecimalPlace();

        for (int i = 0, size = activities.size(); i < size; i++) {
            final Activity activity = activities.get(i);
            // Buy, Sell or Dividend only.
            if (activity.getType() == Activity.Type.Buy) {
                final double quantity = (Double) activity.get(Activity.Param.Quantity);
                final StockInfo stockInfo = (StockInfo) activity.get(Activity.Param.StockInfo);
                this.ROIParams.add(GUIBundle.getString("InvestmentFlowLayerUI_Own") + " "
                        + org.yccheok.jstock.portfolio.Utils.toQuantity(quantity) + " " + stockInfo.symbol);
                final double amount = convertToPoundIfNecessary(stockInfo.code,
                        quantity * this.investmentFlowChartJDialog.getStockPrice(stockInfo.code));
                this.totalROIValue += amount;
                this.ROIValues.add(org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, amount));
            } else if (activity.getType() == Activity.Type.Sell) {
                final double quantity = (Double) activity.get(Activity.Param.Quantity);
                final StockInfo stockInfo = (StockInfo) activity.get(Activity.Param.StockInfo);
                this.ROIParams.add(activity.getType() + " "
                        + org.yccheok.jstock.portfolio.Utils.toQuantity(quantity) + " " + stockInfo.symbol);
                final double amount = convertToPoundIfNecessary(stockInfo.code, activity.getAmount());
                this.totalROIValue += amount;
                this.ROIValues.add(org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, amount));
            } else if (activity.getType() == Activity.Type.Dividend) {
                final StockInfo stockInfo = (StockInfo) activity.get(Activity.Param.StockInfo);
                this.ROIParams.add(activity.getType() + " " + stockInfo.symbol);
                final double amount = activity.getAmount();
                this.totalROIValue += amount;
                this.ROIValues.add(org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, amount));
            } else {
                assert (false);
            }
        }

        final boolean isTotalNeeded = this.ROIParams.size() > 1;
        final String totalParam = GUIBundle.getString("InvestmentFlowLayerUI_Total_Return");
        final String totalValue = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace,
                this.totalROIValue);
        /* This is the height for "total" information. */
        int totalHeight = 0;

        assert (this.ROIParams.size() == this.ROIValues.size());

        int index = 0;
        final int paramValueWidthMargin = 10;
        final int paramValueHeightMargin = 0;
        int maxInfoWidth = -1;
        // paramFontMetrics will always "smaller" than valueFontMetrics.
        int totalInfoHeight = Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight())
                * this.ROIValues.size() + paramValueHeightMargin * (this.ROIValues.size() - 1);
        for (String param : this.ROIParams) {
            final String value = this.ROIValues.get(index++);
            final int paramStringWidth = paramFontMetrics.stringWidth(param + ":") + paramValueWidthMargin
                    + valueFontMetrics.stringWidth(value);
            if (maxInfoWidth < paramStringWidth) {
                maxInfoWidth = paramStringWidth;
            }
        }

        if (isTotalNeeded) {
            final int tmp = paramFontMetrics.stringWidth(totalParam + ":") + paramValueWidthMargin
                    + valueFontMetrics.stringWidth(totalValue);
            if (maxInfoWidth < tmp) {
                maxInfoWidth = tmp;
            }
            totalHeight = Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight());
        }

        final Date date = activities.getDate().getTime();
        final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE, MMMM d, yyyy");
        final String dateString = simpleDateFormat.format(date);
        final int dateStringWidth = dateFontMetrics.stringWidth(dateString);
        final int dateStringHeight = dateFontMetrics.getHeight();
        final int maxStringWidth = Math.max(dateStringWidth, maxInfoWidth);
        final int dateInfoHeightMargin = 5;
        final int infoTotalHeightMargin = 5;
        final int maxStringHeight = isTotalNeeded
                ? (dateStringHeight + dateInfoHeightMargin + totalInfoHeight + infoTotalHeightMargin + totalHeight)
                : (dateStringHeight + dateInfoHeightMargin + totalInfoHeight);

        // Now, We have a pretty good information on maxStringWidth and maxStringHeight.

        final int padding = 5;
        final int boxPointMargin = 8;
        final int width = maxStringWidth + (padding << 1);
        final int height = maxStringHeight + (padding << 1);

        final int borderWidth = width + 2;
        final int borderHeight = height + 2;
        // On left side of the ball.
        final double suggestedBorderX = this.ROIPoint.getX() - borderWidth - boxPointMargin;
        final double suggestedBorderY = this.ROIPoint.getY() - (borderHeight >> 1);
        final double bestBorderX = suggestedBorderX > this.drawArea.getX()
                ? (suggestedBorderX + borderWidth) < (this.drawArea.getX() + this.drawArea.getWidth())
                        ? suggestedBorderX
                        : this.drawArea.getX() + this.drawArea.getWidth() - borderWidth - boxPointMargin
                : this.ROIPoint.getX() + boxPointMargin;
        final double bestBorderY = suggestedBorderY > this.drawArea.getY()
                ? (suggestedBorderY + borderHeight) < (this.drawArea.getY() + this.drawArea.getHeight())
                        ? suggestedBorderY
                        : this.drawArea.getY() + this.drawArea.getHeight() - borderHeight - boxPointMargin
                : this.drawArea.getY() + boxPointMargin;

        final double x = bestBorderX + 1;
        final double y = bestBorderY + 1;

        this.ROIRect.setRect(x, y, width, height);
    }

    private void updateInvestInformationBox(Graphics2D g2) {
        final Font oldFont = g2.getFont();
        final Font paramFont = oldFont;
        final FontMetrics paramFontMetrics = g2.getFontMetrics(paramFont);
        final Font valueFont = oldFont.deriveFont(oldFont.getStyle() | Font.BOLD, (float) oldFont.getSize() + 1);
        final FontMetrics valueFontMetrics = g2.getFontMetrics(valueFont);
        final Font dateFont = oldFont.deriveFont((float) oldFont.getSize() - 1);
        final FontMetrics dateFontMetrics = g2.getFontMetrics(dateFont);

        final Activities activities = this.investmentFlowChartJDialog.getInvestActivities(this.investPointIndex);

        this.investValues.clear();
        this.investParams.clear();
        this.totalInvestValue = 0.0;

        final DecimalPlace decimalPlace = JStock.instance().getJStockOptions().getDecimalPlace();

        for (int i = 0, size = activities.size(); i < size; i++) {
            final Activity activity = activities.get(i);
            // Buy only.
            this.investParams.add(activity.getType() + " "
                    + org.yccheok.jstock.portfolio.Utils.toQuantity(activity.get(Activity.Param.Quantity)) + " "
                    + ((StockInfo) activity.get(Activity.Param.StockInfo)).symbol);

            if (activity.getType() == Activity.Type.Buy) {
                final StockInfo stockInfo = (StockInfo) activity.get(Activity.Param.StockInfo);

                final double amount = convertToPoundIfNecessary(stockInfo.code, activity.getAmount());
                this.totalInvestValue += amount;
                this.investValues
                        .add(org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace, amount));
            } else {
                assert (false);
            }
        }

        final boolean isTotalNeeded = this.investParams.size() > 1;
        final String totalParam = GUIBundle.getString("InvestmentFlowLayerUI_Total_Invest");
        final String totalValue = org.yccheok.jstock.portfolio.Utils.toCurrencyWithSymbol(decimalPlace,
                this.totalInvestValue);
        /* This is the height for "total" information. */
        int totalHeight = 0;

        assert (this.investValues.size() == this.investParams.size());

        int index = 0;
        final int paramValueWidthMargin = 10;
        final int paramValueHeightMargin = 0;
        int maxInfoWidth = -1;
        // paramFontMetrics will always "smaller" than valueFontMetrics.
        int totalInfoHeight = Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight())
                * this.investValues.size() + paramValueHeightMargin * (this.investValues.size() - 1);
        for (String param : this.investParams) {
            final String value = this.investValues.get(index++);
            final int paramStringWidth = paramFontMetrics.stringWidth(param + ":") + paramValueWidthMargin
                    + valueFontMetrics.stringWidth(value);
            if (maxInfoWidth < paramStringWidth) {
                maxInfoWidth = paramStringWidth;
            }
        }

        if (isTotalNeeded) {
            final int tmp = paramFontMetrics.stringWidth(totalParam + ":") + paramValueWidthMargin
                    + valueFontMetrics.stringWidth(totalValue);
            if (maxInfoWidth < tmp) {
                maxInfoWidth = tmp;
            }
            totalHeight = Math.max(paramFontMetrics.getHeight(), valueFontMetrics.getHeight());
        }

        final Date date = activities.getDate().getTime();
        final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE, MMMM d, yyyy");
        final String dateString = simpleDateFormat.format(date);
        final int dateStringWidth = dateFontMetrics.stringWidth(dateString);
        final int dateStringHeight = dateFontMetrics.getHeight();
        final int maxStringWidth = Math.max(dateStringWidth, maxInfoWidth);
        final int dateInfoHeightMargin = 5;
        final int infoTotalHeightMargin = 5;
        final int maxStringHeight = isTotalNeeded
                ? (dateStringHeight + dateInfoHeightMargin + totalInfoHeight + infoTotalHeightMargin + totalHeight)
                : (dateStringHeight + dateInfoHeightMargin + totalInfoHeight);

        // Now, We have a pretty good information on maxStringWidth and maxStringHeight.

        final int padding = 5;
        final int boxPointMargin = 8;
        final int width = maxStringWidth + (padding << 1);
        final int height = maxStringHeight + (padding << 1);
        // On left side of the ball.
        final int suggestedX = (int) (this.investPoint.getX() - width - boxPointMargin);
        final int suggestedY = (int) (this.investPoint.getY() - (height >> 1));
        final int x = suggestedX > this.drawArea.getX()
                ? (suggestedX + width) < (this.drawArea.getX() + this.drawArea.getWidth()) ? suggestedX
                        : (int) (this.drawArea.getX() + this.drawArea.getWidth() - width - boxPointMargin)
                : (int) (investPoint.getX() + boxPointMargin);
        final int y = suggestedY > this.drawArea.getY()
                ? (suggestedY + height) < (this.drawArea.getY() + this.drawArea.getHeight()) ? suggestedY
                        : (int) (this.drawArea.getY() + this.drawArea.getHeight() - height - boxPointMargin)
                : (int) (this.drawArea.getY() + boxPointMargin);

        this.investRect.setRect(x, y, width, height);
    }

    private boolean updateROIPoint(Point2D _ROIPoint) {
        if (_ROIPoint == null) {
            return false;
        }

        final ChartPanel chartPanel = this.investmentFlowChartJDialog.getChartPanel();
        final JFreeChart chart = chartPanel.getChart();
        final XYPlot plot = (XYPlot) chart.getPlot();
        // Dataset 0 are the invest information. 1 is the ROI information.
        final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset(1);
        final TimeSeries timeSeries = timeSeriesCollection.getSeries(0);

        // I also not sure why. This is what are being done in Mouse Listener Demo 4.
        //
        // Don't use it. It will cause us to lose precision.
        //final Point2D p2 = chartPanel.translateScreenToJava2D((Point)_ROIPoint);

        /* Try to get correct main chart area. */
        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea();

        /* Believe it? When there is another thread keep updateing time series data,
         * and keep calling setDirty, _plotArea can be 0 size sometimes. Ignore it.
         * Just assume we had processed it.
         */
        if (_plotArea.getWidth() == 0.0 && _plotArea.getHeight() == 0.0) {
            /* Cheat the caller. */
            return true;
        }

        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();
        final double coordinateX = domainAxis.java2DToValue(_ROIPoint.getX(), _plotArea, domainAxisEdge);

        int low = 0;
        int high = timeSeries.getItemCount() - 1;
        Date date = new Date((long) coordinateX);
        final long time = date.getTime();
        long bestDistance = Long.MAX_VALUE;
        int bestMid = 0;

        while (low <= high) {
            int mid = (low + high) >>> 1;

            final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(mid);
            final Day day = (Day) timeSeriesDataItem.getPeriod();
            final long search = day.getFirstMillisecond();
            final long cmp = search - time;

            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            } else {
                bestDistance = 0;
                bestMid = mid;
                break;
            }

            final long abs_cmp = Math.abs(cmp);
            if (abs_cmp < bestDistance) {
                bestDistance = abs_cmp;
                bestMid = mid;
            }
        }

        final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(bestMid);
        final double xValue = timeSeriesDataItem.getPeriod().getFirstMillisecond();
        final double yValue = timeSeriesDataItem.getValue().doubleValue();
        final double xJava2D = domainAxis.valueToJava2D(xValue, _plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, _plotArea, rangeAxisEdge);

        final int tmpIndex = bestMid;
        // Do not perform translation as this will cause precision losing.
        // We might experience unstable point. For example,
        //
        // this.ROIPoint is 700.9, there are 2 data points which are 700 and
        // 701.
        // During first updateROIPoint(this.ROIPoint) call, data point 701
        // will be chosen, and this.ROIPoint has been truncated to 700.
        // During second updateROIPoint(this.ROIPoint) call, data point 700
        // will be chosen. We may observe an unstable point swings between 700
        // and 701.
        //
        // translateJava2DToScreen will internally convert Point2D.Double to Point.
        //final Point2D tmpPoint = chartPanel.translateJava2DToScreen(new Point2D.Double(xJava2D, yJava2D));
        final Point2D tmpPoint = new Point2D.Double(xJava2D, yJava2D);
        this.drawArea.setRect(_plotArea);

        if (this.drawArea.contains(tmpPoint)) {
            this.ROIPointIndex = tmpIndex;
            this.ROIPoint = tmpPoint;
            return true;
        }
        return false;
    }

    public void updateROIPoint() {
        if (this.updateROIPoint(this.ROIPoint) == false) {
            /* Clear the point. */
            this.ROIPoint = null;
            this.ROIPointIndex = -1;
        }
        this.setDirty(true);
    }

    public void updateInvestPoint() {
        if (this.updateInvestPoint(this.investPoint) == false) {
            /* Clear the point. */
            this.investPoint = null;
            this.investPointIndex = -1;
        }
        this.setDirty(true);
    }

    // Update this.drawArea, this.investPoint and this.investPointIndex.
    private boolean updateInvestPoint(Point2D _investPoint) {
        if (_investPoint == null) {
            return false;
        }

        final ChartPanel chartPanel = this.investmentFlowChartJDialog.getChartPanel();
        final JFreeChart chart = chartPanel.getChart();
        final XYPlot plot = (XYPlot) chart.getPlot();
        final TimeSeriesCollection timeSeriesCollection = (TimeSeriesCollection) plot.getDataset();
        final TimeSeries timeSeries = timeSeriesCollection.getSeries(0);

        // I also not sure why. This is what are being done in Mouse Listener Demo 4.
        //
        // Don't use it. It will cause us to lose precision.
        //final Point2D p2 = chartPanel.translateScreenToJava2D((Point)_investPoint);

        /* Try to get correct main chart area. */
        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea();

        /* Believe it? When there is another thread keep updateing time series data,
         * and keep calling setDirty, _plotArea can be 0 size sometimes. Ignore it.
         * Just assume we had processed it.
         */
        if (_plotArea.getWidth() == 0.0 && _plotArea.getHeight() == 0.0) {
            /* Cheat the caller. */
            return true;
        }

        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();
        final double coordinateX = domainAxis.java2DToValue(_investPoint.getX(), _plotArea, domainAxisEdge);

        int low = 0;
        int high = timeSeries.getItemCount() - 1;
        Date date = new Date((long) coordinateX);
        final long time = date.getTime();
        long bestDistance = Long.MAX_VALUE;
        int bestMid = 0;

        while (low <= high) {
            int mid = (low + high) >>> 1;

            final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(mid);
            final Day day = (Day) timeSeriesDataItem.getPeriod();
            final long search = day.getFirstMillisecond();
            final long cmp = search - time;

            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            } else {
                bestDistance = 0;
                bestMid = mid;
                break;
            }

            final long abs_cmp = Math.abs(cmp);
            if (abs_cmp < bestDistance) {
                bestDistance = abs_cmp;
                bestMid = mid;
            }
        }

        final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(bestMid);
        final double xValue = timeSeriesDataItem.getPeriod().getFirstMillisecond();
        final double yValue = timeSeriesDataItem.getValue().doubleValue();
        final double xJava2D = domainAxis.valueToJava2D(xValue, _plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, _plotArea, rangeAxisEdge);

        final int tmpIndex = bestMid;
        // Do not perform translation as this will cause precision losing.
        // We might experience unstable point. For example,
        //
        // this.investPoint is 700.9, there are 2 data points which are 700 and
        // 701.
        // During first updateInvestPoint(this.investPoint) call, data point 701
        // will be chosen, and this.investPoint has been truncated to 700.
        // During second updateInvestPoint(this.investPoint) call, data point 700
        // will be chosen. We may observe an unstable point swings between 700
        // and 701.
        //
        // translateJava2DToScreen will internally convert Point2D.Double to Point.
        //final Point2D tmpPoint = chartPanel.translateJava2DToScreen(new Point2D.Double(xJava2D, yJava2D));
        final Point2D tmpPoint = new Point2D.Double(xJava2D, yJava2D);
        this.drawArea.setRect(_plotArea);

        if (this.drawArea.contains(tmpPoint)) {
            this.investPointIndex = tmpIndex;
            this.investPoint = tmpPoint;
            return true;
        }
        return false;
    }

    private void processEvent(MouseEvent e, JXLayer layer) {
        if (MouseEvent.MOUSE_DRAGGED == e.getID()) {
            return;
        }

        if (MouseEvent.MOUSE_CLICKED == e.getID()) {
            // Transfer focus to chart if user clicks on the chart.
            this.investmentFlowChartJDialog.getChartPanel().requestFocus();
        }

        final Point mousePoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), layer);

        final boolean status0 = this.updateInvestPoint(mousePoint);
        final boolean status1 = this.updateROIPoint(mousePoint);

        if (status0 || status1) {
            this.setDirty(true);
        }
    }

    @Override
    public void setDirty(boolean isDirty) {
        super.setDirty(isDirty);
    }

    private void drawBusyBox(Graphics2D g2, JXLayer<? extends V> layer) {
        final Font oldFont = g2.getFont();
        final Font font = oldFont;
        final FontMetrics fontMetrics = g2.getFontMetrics(font);

        // Not sure why. Draw GIF image on JXLayer, will cause endless setDirty
        // being triggered by system.
        //final Image image = ((ImageIcon)Icons.BUSY).getImage();
        //final int imgWidth = Icons.BUSY.getIconWidth();
        //final int imgHeight = Icons.BUSY.getIconHeight();
        //final int imgMessageWidthMargin = 5;
        final int imgWidth = 0;
        final int imgHeight = 0;
        final int imgMessageWidthMargin = 0;

        final String message = MessagesBundle.getString("info_message_retrieving_latest_stock_price");
        final int maxWidth = imgWidth + imgMessageWidthMargin + fontMetrics.stringWidth(message);
        final int maxHeight = Math.max(imgHeight, fontMetrics.getHeight());

        final int padding = 5;
        final int width = maxWidth + (padding << 1);
        final int height = maxHeight + (padding << 1);
        final int x = (int) this.drawArea.getX() + (((int) this.drawArea.getWidth() - width) >> 1);
        final int y = (int) this.drawArea.getY() + (((int) this.drawArea.getHeight() - height) >> 1);

        final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
        final Composite oldComposite = g2.getComposite();
        final Color oldColor = g2.getColor();

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setColor(COLOR_BORDER);
        g2.drawRoundRect(x - 1, y - 1, width + 1, height + 1, 15, 15);
        g2.setColor(COLOR_BACKGROUND);
        g2.setComposite(Utils.makeComposite(0.75f));
        g2.fillRoundRect(x, y, width, height, 15, 15);
        g2.setComposite(oldComposite);
        g2.setColor(oldColor);

        //g2.drawImage(image, x + padding, y + ((height - imgHeight) >> 1), layer.getView());

        g2.setFont(font);
        g2.setColor(COLOR_BLUE);

        int yy = y + ((height - fontMetrics.getHeight()) >> 1) + fontMetrics.getAscent();
        g2.setFont(font);
        g2.setColor(COLOR_BLUE);
        g2.drawString(message, x + padding + imgWidth + imgMessageWidthMargin, yy);
        g2.setColor(oldColor);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
        g2.setFont(oldFont);
    }

    @Override
    protected void paintLayer(Graphics2D g2, JXLayer<? extends V> layer) {
        super.paintLayer(g2, layer);

        final ChartPanel chartPanel = ((ChartPanel) layer.getView());

        final Rectangle2D _plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea();

        this.drawArea.setRect(_plotArea);

        if (false == this.investmentFlowChartJDialog.isFinishLookUpPrice()) {
            this.drawBusyBox(g2, layer);
        }

        if (this.investPoint != null) {
            final int RADIUS = 8;
            final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
            final Color oldColor = g2.getColor();

            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setColor(COLOR_RED);
            g2.fillOval((int) (this.investPoint.getX() - (RADIUS >> 1) + 0.5),
                    (int) (this.investPoint.getY() - (RADIUS >> 1) + 0.5), RADIUS, RADIUS);
            g2.setColor(oldColor);
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
            this.updateInvestInformationBox(g2);
        }

        if (this.ROIPoint != null) {
            final int RADIUS = 8;
            final Object oldValueAntiAlias = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
            final Color oldColor = g2.getColor();

            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setColor(COLOR_BLUE);
            g2.fillOval((int) (this.ROIPoint.getX() - (RADIUS >> 1) + 0.5),
                    (int) (this.ROIPoint.getY() - (RADIUS >> 1) + 0.5), RADIUS, RADIUS);
            g2.setColor(oldColor);
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldValueAntiAlias);
            this.updateROIInformationBox(g2);
        }

        this.solveConflict();

        if (this.investPoint != null) {
            this.drawInformationBox(g2, this.investmentFlowChartJDialog.getInvestActivities(this.investPointIndex),
                    investRect, investParams, investValues,
                    GUIBundle.getString("InvestmentFlowLayerUI_Total_Invest"), totalInvestValue,
                    COLOR_RED_BACKGROUND, COLOR_RED_BORDER);
        }

        if (this.ROIPoint != null) {
            this.drawInformationBox(g2, this.investmentFlowChartJDialog.getROIActivities(this.ROIPointIndex),
                    ROIRect, ROIParams, ROIValues, GUIBundle.getString("InvestmentFlowLayerUI_Total_Return"),
                    totalROIValue, COLOR_BLUE_BACKGROUND, COLOR_BLUE_BORDER);
        }

        this.drawTitle(g2);
    }

    private void solveConflict() {
        if (this.investPoint == null || this.ROIPoint == null) {
            /* No conflict to be solved. */
            return;
        }

        /* Take border into consideration. */
        final Rectangle ROIRectWithBorder = new Rectangle((Rectangle) this.ROIRect);
        final Rectangle investRectWithBorder = new Rectangle((Rectangle) this.investRect);
        ROIRectWithBorder.setLocation((int) ROIRectWithBorder.getX() - 1, (int) ROIRectWithBorder.getY() - 1);
        ROIRectWithBorder.setSize((int) ROIRectWithBorder.getWidth() + 2, (int) ROIRectWithBorder.getHeight() + 2);
        investRectWithBorder.setLocation((int) investRectWithBorder.getX() - 1,
                (int) investRectWithBorder.getY() - 1);
        investRectWithBorder.setSize((int) investRectWithBorder.getWidth() + 2,
                (int) investRectWithBorder.getHeight() + 2);
        if (false == ROIRectWithBorder.intersects(investRectWithBorder)) {
            return;
        }

        final Rectangle oldROIRect = new Rectangle((Rectangle) this.ROIRect);
        final Rectangle oldInvestRect = new Rectangle((Rectangle) this.investRect);

        // Move to Down.
        if (this.ROIRect.getY() > this.investRect.getY()) {
            ((Rectangle) this.ROIRect).translate(0,
                    (int) (this.investRect.getY() + this.investRect.getHeight() - this.ROIRect.getY() + 4));
        } else {
            ((Rectangle) this.investRect).translate(0,
                    (int) (this.ROIRect.getY() + this.ROIRect.getHeight() - this.investRect.getY() + 4));
        }

        if ((this.drawArea.getY() + this.drawArea.getHeight()) > (this.ROIRect.getY() + this.ROIRect.getHeight())
                && (this.drawArea.getY() + this.drawArea.getHeight()) > (this.investRect.getY()
                        + this.investRect.getHeight())) {
            return;
        }

        this.ROIRect.setRect(oldROIRect);
        this.investRect.setRect(oldInvestRect);

        // Move to Up.
        if (this.ROIRect.getY() > this.investRect.getY()) {
            ((Rectangle) this.investRect).translate(0,
                    -(int) (this.investRect.getY() + this.investRect.getHeight() - this.ROIRect.getY() + 4));
        } else {
            ((Rectangle) this.ROIRect).translate(0,
                    -(int) (this.ROIRect.getY() + this.ROIRect.getHeight() - this.investRect.getY() + 4));
        }

        if ((this.drawArea.getY() < this.ROIRect.getY()) && (this.drawArea.getY() < this.investRect.getY())) {
            return;
        }

        this.ROIRect.setRect(oldROIRect);
        this.investRect.setRect(oldInvestRect);
    }

    @Override
    protected void processMouseEvent(MouseEvent e, JXLayer<? extends V> layer) {
        super.processMouseEvent(e, layer);
        this.processEvent(e, layer);
    }

    @Override
    protected void processMouseMotionEvent(MouseEvent e, JXLayer<? extends V> layer) {
        super.processMouseMotionEvent(e, layer);
        this.processEvent(e, layer);
    }

    @Override
    public void processKeyEvent(java.awt.event.KeyEvent e, JXLayer<? extends V> l) {
        if (e.getID() != KeyEvent.KEY_PRESSED) {
            // We are only interested in KEY_PRESSED event.
            return;
        }
        final int code = e.getKeyCode();
        switch (code) {
        case KeyEvent.VK_LEFT:
            this.updatePointAndIndexIfPossible(-1);
            break;
        case KeyEvent.VK_RIGHT:
            this.updatePointAndIndexIfPossible(+1);
            break;
        case KeyEvent.VK_UP:
            // Switch to another stock.
            this.investmentFlowChartJDialog.selectPreviousJComboBoxSelection();
            break;
        case KeyEvent.VK_DOWN:
            // Switch to another stock.
            this.investmentFlowChartJDialog.selectNextJComboBoxSelection();
            break;
        }
    }

    // Each time, only either ROI or Invest point will be offset. But not both.
    // To determine which of them shall be offset, the resultant offset point 
    // shall kept the distance among ROI and Invest point minimal.
    private void updatePointAndIndexIfPossible(int dataOffset) {
        Type updatedType = null;

        boolean needToReturnEarly = false;

        // If there is at least one point (Either ROI or Invest) absent from the
        // screen, we will show them up and return early.
        if (this.investPointIndex < 0) {
            this.investPointIndex = 0;
            this.investPoint = getPoint(this.investPointIndex, Type.Invest);
            this.updateInvestPoint();
            needToReturnEarly = true;
        }
        if (this.ROIPointIndex < 0) {
            this.ROIPointIndex = dataOffset + this.ROIPointIndex;
            this.ROIPoint = getPoint(this.ROIPointIndex, Type.ROI);
            this.updateROIPoint();
            needToReturnEarly = true;
        }

        if (needToReturnEarly) {
            return;
        }

        int tmpROIPointIndex = -1;
        int tmpInvestPointIndex = -1;
        Point2D tmpROIPoint = null;
        Point2D tmpInvestPoint = null;
        if (updatedType == null) {
            tmpROIPointIndex = dataOffset + this.ROIPointIndex;
            tmpInvestPointIndex = dataOffset + this.investPointIndex;
            tmpROIPoint = getPoint(tmpROIPointIndex, Type.ROI);
            tmpInvestPoint = getPoint(tmpInvestPointIndex, Type.Invest);

            if (tmpROIPoint == null) {
                if (tmpInvestPoint == null) {
                    // No update can be performed. Returns early.
                    return;
                }
                updatedType = Type.Invest;
            } else if (tmpInvestPoint == null) {
                assert (tmpROIPoint != null);
                updatedType = Type.ROI;
            } else {
                // Distance check.
                double d0 = Math.abs(tmpROIPoint.getX() - this.investPoint.getX());
                double d1 = Math.abs(tmpInvestPoint.getX() - this.ROIPoint.getX());
                if (d0 < d1) {
                    updatedType = Type.ROI;
                } else {
                    updatedType = Type.Invest;
                }
            }
        }

        assert (updatedType != null);

        if (updatedType == Type.ROI) {
            this.ROIPointIndex = dataOffset + this.ROIPointIndex;
            this.ROIPoint = tmpROIPoint != null ? tmpROIPoint : getPoint(this.ROIPointIndex, Type.ROI);
            this.updateROIPoint();
        } else {
            assert (updatedType == Type.Invest);
            this.investPointIndex = dataOffset + this.investPointIndex;
            this.investPoint = tmpInvestPoint != null ? tmpInvestPoint
                    : getPoint(this.investPointIndex, Type.Invest);
            this.updateInvestPoint();
        }
    }

    // Get the mouse coordinate, based on given data index. Returns null, if
    // the given data index is out of data range.
    private Point2D.Double getPoint(int dataIndex, Type type) {
        if (dataIndex < 0) {
            return null;
        }

        final ChartPanel chartPanel = this.investmentFlowChartJDialog.getChartPanel();
        final JFreeChart chart = chartPanel.getChart();
        final XYPlot plot = (XYPlot) chart.getPlot();
        // Dataset 0 are the invest information. 1 is the ROI information.
        final TimeSeriesCollection timeSeriesCollection;
        if (type == Type.Invest) {
            timeSeriesCollection = (TimeSeriesCollection) plot.getDataset(0);
        } else {
            assert (type == Type.ROI);
            timeSeriesCollection = (TimeSeriesCollection) plot.getDataset(1);
        }
        final TimeSeries timeSeries = timeSeriesCollection.getSeries(0);

        if (dataIndex >= timeSeries.getItemCount()) {
            /* Not ready yet. */
            return null;
        }

        final ValueAxis domainAxis = plot.getDomainAxis();
        final RectangleEdge domainAxisEdge = plot.getDomainAxisEdge();
        final ValueAxis rangeAxis = plot.getRangeAxis();
        final RectangleEdge rangeAxisEdge = plot.getRangeAxisEdge();

        final TimeSeriesDataItem timeSeriesDataItem = timeSeries.getDataItem(dataIndex);
        final double xValue = timeSeriesDataItem.getPeriod().getFirstMillisecond();
        final double yValue = timeSeriesDataItem.getValue().doubleValue();
        final Rectangle2D plotArea = chartPanel.getChartRenderingInfo().getPlotInfo().getDataArea();
        final double xJava2D = domainAxis.valueToJava2D(xValue, plotArea, domainAxisEdge);
        final double yJava2D = rangeAxis.valueToJava2D(yValue, plotArea, rangeAxisEdge);
        // Use Double version, to avoid from losing precision.
        return new Point2D.Double(xJava2D, yJava2D);
    }
}