/*  Monte-Carlo simulation code for statistical physics
    Copyright (C) 2001-2004  Sylvain Reynal
    
    Département de Physique
    Ecole Nationale Supérieure de l'Electronique et de ses Applications (ENSEA)
    6, avenue du Ponceau,    
    F-95014 CERGY CEDEX

    et
    
    Laboratoire de Physique Théorique et Modélisation (LPTM)
    Université de Cergy-Pontoise - Site de Neuville
    F-95031 CERGY CEDEX
    
	
    Tel : 00 +33 130 736 245
    Fax : 00 +33 130 736 667
    e-mail : reynal@ensea.fr
    web page : http://www.ensea.fr/staff/reynal/
*/

package fr.ensea.chart;

import fr.ensea.Localizer;
import JSci.swing.*;
import javax.swing.*;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Color;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.*;
import java.util.*;
import java.text.*;

/**
 * An AWT-style plotter for Curve2D's, with double-buffering functionality.
 * Supports multiple curves and plot-styles, as well as axes, numbering and labelling.
 */
public class Plot2D extends JDoubleBufferedComponent implements UIConstants {

	DecoratedCurve2D[] curveArray = new DecoratedCurve2D[0]; // list of curves to be drawn

	/* --- axis and scales --- */
	Point ptOrg = new Point(); // coordinates in pixels of the (0,0) point
	Point2D scale=new Point2D(); // scale factor along X- and Y- axis between model and screen coordinates
	Axis xAxis, yAxis;
	int leftMarginPx, bottomMarginPx, rightMarginPx, topMarginPx; // space around graph frame
	static final int DEFAULT_MARGIN=10; // in pixels
	int tickLengthPx = 10; // length of a tick mark, in pixels
	Dimension preferredSize; // component's pref size, in px.
	Rectangle paintCurvesClip; // clip set when painting curves only ; updated in dataChanged();

	/* --- labels and colors --- */
	String title, xAxisLabel;
	Color axesColor, gridColor;


	/* --- fonts --- */
	/* package access from Axis */ FontMetrics fontMetrics;
	private int fontHeight;
	
	/**
	 * Constructs a new graph with the default colors.
	 */
	public Plot2D() {
		this(Color.black, Color.white, Color.darkGray);
	}

	/**
	 * Constructs a new graph with the given colors.
	 */
	public Plot2D(Color backGround, Color axesColor, Color gridColor) {
		setBackground(backGround);
		setForeground(Color.yellow);
		this.axesColor = axesColor;
		this.gridColor = gridColor;
		fontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(new Font("SansSerif", Font.PLAIN, 10));
		fontHeight = fontMetrics.getHeight();
		bottomMarginPx = DEFAULT_MARGIN + fontHeight;
		topMarginPx = leftMarginPx = rightMarginPx = DEFAULT_MARGIN;
		xAxis=new Axis(Curve2D.X_AXIS);
		yAxis=new Axis(Curve2D.Y_AXIS);
		addComponentListener(new ComponentHandler());
		addMouseListener(new MouseHandler());
	}

	///////////////////////////////////////////////////////////////////////////////////////////
	//// Curve management
	///////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Add the given curve to the list of curves
	 */
	public void addCurve(Curve2D c, String title, Color color, int plotStyle){
		addDecoratedCurve(new DecoratedCurve2D(c, title, color, plotStyle));
	}

	/**
	 * Add the given curve to the list of curves
	 */
	public void addDecoratedCurve(DecoratedCurve2D c){
		DecoratedCurve2D[] _curveArray = new DecoratedCurve2D[curveArray.length+1];
		System.arraycopy(curveArray, 0, _curveArray, 0, curveArray.length);
		_curveArray[curveArray.length] = c;
		curveArray = _curveArray;
		dataChanged();
	}

	public DecoratedCurve2D getDecoratedCurve(int idx){
		return curveArray[idx];
	}

	/**
	 * Remove all the curves from this graph
	 */
	public void removeCurves(){
		curveArray = new DecoratedCurve2D[0];
		dataChanged();
	}

	/**
	 * Remove the last curve that was added to the graph
	 */
	public void removeLastCurve(){
		if (curveArray.length==0) return;
		DecoratedCurve2D[] _curveArray = new DecoratedCurve2D[curveArray.length-1];
		System.arraycopy(curveArray,0,_curveArray,0,_curveArray.length);
		curveArray = _curveArray;
		dataChanged();
	}

	/** return the number of curves */
	public int getCurvesCount(){
		return curveArray.length;
	}

	///////////////////////////////////////////////////////////////////////////////////////////
	//// Plot parameters
	///////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Set the title
	 */
	public void setTitle(String title){
		this.title = title;
		if (title!=null) topMarginPx = fontHeight + DEFAULT_MARGIN;
		else topMarginPx = DEFAULT_MARGIN;

		dataChanged();
	}

	/**
	 * Returns  the plot's title
	 */
	public String getTitle(){
		return this.title;
	}

	/**
	 * Set the X-axis label
	 */
	public void setXaxisLabel(String xAxisLabel){
		this.xAxisLabel = xAxisLabel;
		if (xAxisLabel!=null) bottomMarginPx = 2 * fontHeight + 2 * DEFAULT_MARGIN;
		else bottomMarginPx = DEFAULT_MARGIN + fontHeight;

		dataChanged();
	}

	/**
	 * Returns the X-axis label
	 * @return null if there's no x-axis label
	 */
	public String getXaxisLabel(){
		return xAxisLabel;
	}
	
	public Axis xAxis(){
		return this.xAxis;
	}

	public Axis yAxis(){
		return this.yAxis;
	}

	/**
	 * Set the color for the axes and the grid. 
	 */
	public void setColors(Color axesColor, Color gridColor){
		this.axesColor = axesColor;
		this.gridColor = gridColor;
		redraw();
	}

	/**
	 * Sets the plot style
	 * @param curveIdx a curve index, or "-1" to change all curves at one.
	 * @param style DOTS, LINES or STAIRS
	 */
	public void setPlotStyle(int curveIdx, int style){
		if (curveIdx==-1){
			for (int i=0; i<curveArray.length; i++) curveArray[i].setPlotStyle(style);
		}
		else curveArray[curveIdx].setPlotStyle(style);
		dataChanged();
	}

	/**
	 * Return the plot style for the given curve
	 */
	public int getPlotStyle(int curveIdx){
		return curveArray[curveIdx].getPlotStyle();
	}




	///////////////////////////////////////////////////////////////////////////////////////////
	//// Model <-> Screen
	///////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Converts a data point to screen coordinates.
	 */
	public Point toScreenCoordinates(Point2D p) {
		return new Point((int)(ptOrg.x + scale.x * p.x), (int)(ptOrg.y - scale.y * p.y));
	}

	/**
	 * Converts a data point to screen coordinates.
	 */
	public Point toScreenCoordinates(double x, double y) {
		return new Point((int)(ptOrg.x + scale.x * x), (int)(ptOrg.y - scale.y * y));
	}

	/**
	 * Converts a screen point to data coordinates.
	 */
	public Point2D toModelCoordinates(Point p) {
		return new Point2D((p.x - ptOrg.x) / scale.x, (ptOrg.y - p.y) / scale.y);
	}

	/**
	 * Converts a screen point to data coordinates.
	 */
	public Point2D toModelCoordinates(int x, int y) {
		return new Point2D((x - ptOrg.x) / scale.x, (ptOrg.y - y) / scale.y);
	}

	/**
	 * Draws a line from p1 to p2, where the two given points are in model coordinates.
	 */
	private void drawLine(Graphics g, double p1x, double p1y, double p2x, double p2y){
		Point pA = toScreenCoordinates(p1x, p1y);
		Point pB = toScreenCoordinates(p2x, p2y);
		g.drawLine(pA.x, pA.y, pB.x, pB.y);
	}

	/**
	 * Draws a line from p1 to p2, where the two given points are in model coordinates.
	 */
	private void drawRect(Graphics g, double p1x, double p1y, double p2x, double p2y, boolean filled){
		Point pA = toScreenCoordinates(p1x, p1y);
		Point pB = toScreenCoordinates(p2x, p2y);
		int x, y, w, h;
		if (pB.x > pA.x){
			x = pA.x;
			w = pB.x - pA.x;
		}
		else {
			x = pB.x;
			w = pA.x - pB.x;
		}
		if (pB.y > pA.y){
			y = pA.y;
			h = pB.y - pA.y;
		}
		else {
			y = pB.y;
			h = pA.y - pB.y;
		}
		if (filled) g.fillRect(x,y,w,h);
		else g.drawRect(x,y,w,h);
	}

	/**
	 * Draws a line from p1 to p2, where the two given points are in model coordinates.
	 */
	private void drawDot(Graphics g, double p1x, double p1y){
		Point pA = toScreenCoordinates(p1x, p1y);
		//g.drawLine(pA.x, pA.y, pA.x, pA.y);
		g.fillOval(pA.x-3, pA.y-3, 6,6);
	}

	///////////////////////////////////////////////////////////////////////////////////////////
	//// Axis, ticks, margins and all that...
	///////////////////////////////////////////////////////////////////////////////////////////


	/**
	 * Invoke this method to signal that some curve's data have changed, e.g. implying that axis bounds
	 * may have to be recomputed.
	 */
	public void dataChanged() {
		// possibly determine xAxis.min/Y and xAxis.max/Y from curves
		// + recompute "maxNumberingWidthPx" for each axis (string width in pixel of the largest label)
		xAxis.dataChanged(this);
		yAxis.dataChanged(this);

		// recompute horizontal margins from new extrema values (since they depend on the width of numbering labels)
		rightMarginPx = xAxis.maxNumberingWidthPx; // trailing part of rightmost x-axis numbering
		leftMarginPx = 12 + yAxis.maxNumberingWidthPx;
		int maxWidthOfYAxisLabels = 0; // maximum width in px of Y-axis labels (NOT numberings !)
		for (int i=0; i<curveArray.length; i++){
			if (!curveArray[i].isVisible()) continue;
			int w = fontMetrics.stringWidth(curveArray[i].getTitle());
			if (w > maxWidthOfYAxisLabels) maxWidthOfYAxisLabels = w;
		}
		leftMarginPx += (maxWidthOfYAxisLabels + 10);

		// recompute scaling factors and location of (0,0) point:
		scale.x = (getSize().width - leftMarginPx - rightMarginPx) / (xAxis.max - xAxis.min);
		scale.y = (getSize().height - bottomMarginPx - topMarginPx) / (yAxis.max - yAxis.min);
		ptOrg.x = (int)(- xAxis.min * scale.x + leftMarginPx);
		ptOrg.y = (int)(topMarginPx + yAxis.max * scale.y);
		// recompute xAxis.inc and yAxis.inc: xAxis.maxNumberingWidthPx=max width in pixels of x-axis labels
		// so xAxis.maxNumberingWidthPx/scale.x is the same width, yet in model coordinates
		// and we consider ? times this width to be the optimal distance b/w ticks:
		xAxis.inc = ceilMultiple125(xAxis.maxNumberingWidthPx * 2.0 / scale.x);
		yAxis.inc = ceilMultiple125(40.0/ scale.y);
		//System.out.println("xAxis.maxNumberingWidthPx="+xAxis.maxNumberingWidthPx+", scale.x="+scale.x+", xAxis.inc="+(xAxis.maxNumberingWidthPx * 2.0 / scale.x)+" -> "+xAxis.inc);

		// compute clip to frame area (used by paintCurves)
		this.paintCurvesClip = new Rectangle(toScreenCoordinates(xAxis.min, yAxis.max)); // upper left corner
		this.paintCurvesClip.width = (int)((xAxis.max-xAxis.min)*scale.x);
		this.paintCurvesClip.height = (int)((yAxis.max-yAxis.min)*scale.y);

		// repaint:
		super.redraw();
		//System.out.println("========== Data changed ==========\n"+this.toString());
	}


	/**
	 * Ceils the given double to the lowest double (sub-)multiple of 1, 2 or 5, e.g. 0.12 -> 0.2 and 640 -> 1000.
	 * This method is specially well suited for rounding x- or y-axis increments so that
	 * they are multiple or 1, 2 or 5.
	 * This works with negative number as well (same behaviour as Math.ceil(),except for the rounding to 1,2 or 5)
	 * @return return the lowest multiple of 1, 2 or 5 that is greater than x
	 */
	private static double ceilMultiple125(double x) {
		if (Double.isNaN(x)) return 1.0;
		if (x==0) return x;
		double absx = Math.abs(x);
		// compute magnitude of integer part :
		int mag = (int)Math.floor(Math.log(absx)/Math.log(10)); // e.g. 12.45 -> 1 ; 900.1 -> 2
		// and Math.pow(10,mag) yields : 12.45 -> 10 ; 900.1 -> 100
		double nearestPowerOfTen = Math.pow(10,mag);

		// compute floored integer part :  absx/10^mag lies within [1,10) !
		int n = (int)Math.ceil(absx/nearestPowerOfTen);

		// following examples are for absx in [10,100), whereby nearestPowerOfTen = 10:
		// [10,20) -> 20
		// [20,50) -> 50
		// [50,100) -> 100
		if (n < 1 || n > LOG_CEILED_125.length) {
			System.out.println("------------------------------------------------------------");
			System.out.println("x="+x);
			System.out.println("absx="+absx);
			System.out.println("mag="+mag);
			System.out.println("nearestPowerOfTen="+nearestPowerOfTen);
			System.out.println("n="+n);
			return nearestPowerOfTen*10;
		}
		if (x>0) return  (double)(LOG_CEILED_125[n-1]*nearestPowerOfTen);
		else return -(double)(LOG_FLOORED_125[n-1]*nearestPowerOfTen);
			
	}

	//                                           1  2  3  4  5   6   7   8   9   10
	private final static int[] LOG_CEILED_125 = {1, 2, 5, 5, 5, 10, 10, 10, 10, 10}; // see ceilMultiple125

	/**
	 * Floors the given double to the highest double (sub-)multiple of 1, 2 or 5, e.g. 0.12 -> 0.1 and 640 -> 500.
	 * This method is specially well suited for rounding x- or y-axis increments so that
	 * they are multiple or 1, 2 or 5.
	 * This works with negative number as well (same behaviour as Math.floor(),except for the rounding to 1,2 or 5)
	 * @return return the highest multiple of 1, 2 or 5 that is less than x
	 */
	private static double floorMultiple125(double x) {
		double absx = Math.abs(x);
		// compute magnitude of integer part :
		int mag = (int)Math.floor(Math.log(absx)/Math.log(10)); // e.g. 12.45 -> 1 ; 900.1 -> 2
		// and Math.pow(10,mag) yields : 12.45 -> 10 ; 900.1 -> 100
		double nearestPowerOfTen = Math.pow(10,mag);

		// compute floored integer part :  absx/10^mag lies within [1,10) !
		int n = (int)Math.floor(absx/nearestPowerOfTen);

		// following examples are for absx in [10,100), whereby nearestPowerOfTen = 10:
		// [10,20) -> 10
		// [20,50) -> 20
		// [50,100) -> 50
		if (x>0) return (double)(LOG_FLOORED_125[n-1]*nearestPowerOfTen);
		else return -(double)(LOG_CEILED_125[n-1]*nearestPowerOfTen);
	}

	private final static int[] LOG_FLOORED_125 = {1, 2, 2, 2, 5, 5, 5, 5, 5, 10}; // see floorMultiple125
	
	///////////////////////////////////////////////////////////////////////////////////////////
	//// Paint methods
	///////////////////////////////////////////////////////////////////////////////////////////

	// paint into the offscreen buffer image:
	public void offscreenPaint(Graphics g){
		paintGrid(g);
		paintCurves(g);
		paintFrame(g);
	}

	// paint curves
	private void paintCurves(Graphics g){
		Rectangle oldClip=g.getClipBounds();
		g.clipRect(paintCurvesClip.x, paintCurvesClip.y, paintCurvesClip.width, paintCurvesClip.height);

		// now paint curves:
		Point2D p1, p2;
		double xA,xB;
		for (int curveIdx=0; curveIdx < curveArray.length; curveIdx++){
			if (!curveArray[curveIdx].isVisible()) continue;
			DecoratedCurve2D decorCurve = curveArray[curveIdx];
			Curve2D curve = decorCurve.curve;
			g.setColor(decorCurve.getColor());
			if (curve.getNbPoints()<2){
				for (int i = 0; i < curve.getNbPoints(); i++) {
					p1 = curve.getPoint(i);
					drawDot(g,p1.x, p1.y);
				}
				continue;
			}
			switch (decorCurve.getPlotStyle()){
				// -----------------------------------------------
			case DecoratedCurve2D.LINES:
				p1 = curve.getPoint(0);
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p2 = curve.getPoint(i);
					drawLine(g,p1.x, p1.y, p2.x, p2.y);
					p1 = p2;
				}
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.DOTS:
				for (int i = 0; i < curve.getNbPoints(); i++) {
					p1 = curve.getPoint(i);
					drawDot(g,p1.x, p1.y);
				}
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.DOTS_LINES:
				p1 = curve.getPoint(0);
				drawDot(g,p1.x, p1.y);
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p2 = curve.getPoint(i);
					drawLine(g,p1.x, p1.y, p2.x, p2.y);
					drawDot(g,p2.x, p2.y);
					p1 = p2;

				}
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.STAIRS:
				p1 = curve.getPoint(0);
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p2 = curve.getPoint(i);
					drawLine(g,p1.x, p1.y, p2.x, p1.y);
					drawLine(g,p2.x, p1.y, p2.x, p2.y);
					p1 = p2;
				}
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.BARS: // works well only if x values are ordered
				double yBaseline=0; // where to paint the bar's baseline ; y=0 is ok, except if it doesn't belong to the axis range !
				if (yAxis.min > 0) yBaseline=yAxis.min;
				else if (yAxis.max < 0) yBaseline=yAxis.max;
				p1 = curve.getPoint(0);
				xA=p1.x;
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p2 = curve.getPoint(i);
					xB=(p1.x+p2.x)/2.0;
					drawLine(g, xA, 0, xA, p1.y);
					drawLine(g, xA, p1.y, xB, p1.y);
					drawLine(g, xB, p1.y, xB, 0);
					p1 = p2;
					xA = xB;
				}
				drawLine(g, xA, 0, xA, p1.y);
				drawLine(g, xA, p1.y, p1.x, p1.y);
				drawLine(g, p1.x, p1.y, p1.x, 0);
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.BARS_FILLED:
				p1 = curve.getPoint(0);
				xA=p1.x;
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p2 = curve.getPoint(i);
					xB=(p1.x+p2.x)/2.0;
					g.setColor(decorCurve.getColor()); drawRect(g, xA, 0, xB, p1.y,true);
					g.setColor(getBackground()); drawRect(g, xA, 0, xB, p1.y,false);
					p1 = p2;
					xA = xB;
				}
				g.setColor(decorCurve.getColor()); drawRect(g, xA, 0, p1.x, p1.y,true);
				g.setColor(getBackground()); drawRect(g, xA, 0, p1.x, p1.y,false);
				break;
				// -----------------------------------------------
			case DecoratedCurve2D.SAMPLES:
				for (int i = 1; i < curve.getNbPoints(); i++) {
					p1 = curve.getPoint(i);
					drawLine(g, p1.x, 0, p1.x, p1.y);
				}
				break;
			default:
			}
		}
		g.setClip(oldClip);
	}

	// paint grid lines + axis numberings
	private void paintGrid(Graphics g){
		String label;
		double tickLength;
		int strWidth;
		Point p;
		// draw vertical grid lines + x-axis numbering:
		tickLength = tickLengthPx / scale.y;
		for (double x=Math.ceil(xAxis.min/xAxis.inc) * xAxis.inc; x <= xAxis.max; x += xAxis.inc){
			// grid:
			g.setColor(gridColor);
			drawLine(g, x, yAxis.min, x, yAxis.max); // in model coordinates !
			// ticks:
			g.setColor(axesColor);
			drawLine(g, x, yAxis.min, x, yAxis.min+tickLength);
			drawLine(g, x, yAxis.max, x, yAxis.max-tickLength);
			// labels:
			label=String.valueOf(xAxis.numberingsFormatter.format(x));
			strWidth = fontMetrics.stringWidth(label);
			p = toScreenCoordinates(x, yAxis.min);
			g.drawString(label, p.x - strWidth / 2, p.y + 5 + fontHeight);
		}

		// draw horizonal grid lines + y-axis numbering:
		tickLength = tickLengthPx / scale.x;
		for (double y=Math.ceil(yAxis.min/yAxis.inc) * yAxis.inc; y <= yAxis.max; y += yAxis.inc){
			g.setColor(gridColor);
			drawLine(g, xAxis.min, y, xAxis.max, y);
			g.setColor(axesColor);
			drawLine(g, xAxis.min, y, xAxis.min+tickLength, y);
			drawLine(g, xAxis.max, y, xAxis.max-tickLength, y);
			// labels:
			label=String.valueOf(yAxis.numberingsFormatter.format(y));
			strWidth = fontMetrics.stringWidth(label);
			p = toScreenCoordinates(xAxis.min, y);
			g.drawString(label, p.x - 10 - strWidth, p.y + fontHeight / 4);
		}
	}

	// paint frame, title and axis keys
	private void paintFrame(Graphics g){
		String label;
		int strWidth;
		Point p;

		// draw frame
		p = toScreenCoordinates(xAxis.min, yAxis.max);
		g.setColor(axesColor);
		g.drawRect(p.x, p.y, (int)((xAxis.max-xAxis.min)*scale.x), (int)((yAxis.max-yAxis.min)*scale.y));

		// draw title:
		if (title != null){
			p = toScreenCoordinates((xAxis.min+xAxis.max)/2.0, yAxis.max);
			strWidth = fontMetrics.stringWidth(title);
			g.setColor(getForeground());
			g.drawString(title, p.x-strWidth/2, p.y-8);
		}
		// draw x-axis key:
		if (xAxisLabel != null){
			p = toScreenCoordinates((xAxis.min+xAxis.max)/2.0, yAxis.min);
			strWidth = fontMetrics.stringWidth(xAxisLabel);
			g.setColor(getForeground());
			g.drawString(xAxisLabel, p.x - strWidth/2, p.y + 10 + 2 * fontHeight);
		}
		// draw y-axis keys:
		p = toScreenCoordinates(xAxis.min,(yAxis.max));
		for (int i=0; i<curveArray.length; i++){
			DecoratedCurve2D decorCurve = curveArray[i];
			if (!decorCurve.isVisible()) continue;
			g.setColor(decorCurve.getColor());
			g.drawString(decorCurve.getTitle(), p.x - leftMarginPx + 2, p.y + fontHeight / 4);
			p.y += fontHeight + 2;
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////
	//// Component
	///////////////////////////////////////////////////////////////////////////////////////////

	public Insets getInsets(){
		return new Insets(10,10,10,10);
	}

	/**
	 * Listen to "resize" events and rescale graph accordingly
	 */
	class ComponentHandler extends ComponentAdapter {
		public void componentResized(ComponentEvent e){
			dataChanged();
		}
		public void componentShown(ComponentEvent e){
			dataChanged();
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////
	//// Customizing
	///////////////////////////////////////////////////////////////////////////////////////////

	class Customizer extends JPanel implements ItemListener, ActionListener {
		JTextField titleTF, xAxisLabelTF;
		JComboBox axesColorCH, gridColorCH;

		Customizer(){
			super(new GridLayout(2*(getCurvesCount()+3),1));
			// plot panel:
			add(new JLabel("[Plot2D]"));
			add(createPlotPanel());
			// axis panels:
			add(new JLabel("[X-axis]"));
			add(xAxis.createCustomizer(Plot2D.this));
			add(new JLabel("[Y-axis]"));
			add(yAxis.createCustomizer(Plot2D.this));
			// curves panels:
			for (int i=0; i<getCurvesCount(); i++){
				add(new JLabel("["+Localizer.get("Curve")+" " + i+"]"));
				add(curveArray[i].createCustomizer(Plot2D.this));
			}
		}
		JPanel createPlotPanel(){
			JPanel p = new JPanel(new GridLayout(1,8));
			p.add(new JLabel(Localizer.get("Title")+":"));
			p.add(titleTF=new JTextField(title,10));
			titleTF.addActionListener(this);
			p.add(new JLabel(Localizer.get("XaxisKey")+":"));
			p.add(xAxisLabelTF=new JTextField(xAxisLabel,10));
			xAxisLabelTF.addActionListener(this);
			p.add(new JLabel(Localizer.get("AxesColor")+":"));
			p.add(axesColorCH=createColorCH(axesColor));
			p.add(new JLabel(Localizer.get("GridColor")+":"));
			p.add(gridColorCH=createColorCH(gridColor));
			return p;
		}
			
		JComboBox createColorCH(Color initColor){
			JComboBox colorCH = new JComboBox();
			int selIdx=0;
			for (int i=0; i<PREDEFINED_COLORS.length; i++){
				colorCH.addItem(PREDEFINED_COLORS_NAMES[i]);
				if (initColor.equals(PREDEFINED_COLORS[i])) selIdx=i;
			}
			colorCH.setSelectedIndex(selIdx);
			colorCH.addItemListener(this);
			return colorCH;
		}
		public void actionPerformed(ActionEvent evt){
			if (evt.getSource() == titleTF){
				setTitle(titleTF.getText());
				dataChanged();
			}
			else if (evt.getSource() == xAxisLabelTF){
				xAxisLabel = xAxisLabelTF.getText();
				dataChanged();
			}
		}
		public void itemStateChanged(ItemEvent evt){
			if (evt.getSource() == gridColorCH){
				gridColor = PREDEFINED_COLORS[gridColorCH.getSelectedIndex()];
				dataChanged();
			}
			else if (evt.getSource() == axesColorCH){
				axesColor = PREDEFINED_COLORS[axesColorCH.getSelectedIndex()];
				dataChanged();
			}
		}
	}

	class MouseHandler extends MouseAdapter {
		public void mousePressed(MouseEvent evt){
			if (evt.isPopupTrigger()){
				JFrame f = createCustomizerFrame();
				f.show();
			}
		}
	}
	
	/**
	 * Creates a Panel suited for editing this curve
	 */
	public JFrame createCustomizerFrame(){
		JPanel p = new JPanel();
		p.add(new Customizer());
		JScrollPane sp = new JScrollPane(p);
		JFrame f = new JFrame("Customizer");
		f.addWindowListener(new CustomizerWindowHandler(f));
		//f.getContentPane().setLayout(new BorderLayout(5,5));
		f.getContentPane().add(sp);
		//f.setSize(1000,160+80*getCurvesCount()); // 30 vertical per panel
		//f.setSize(1000,300);
		f.pack();
		return f;
	}
	
	class CustomizerWindowHandler extends WindowAdapter {
		JFrame f;
		CustomizerWindowHandler(JFrame f){
			this.f=f;
		}
		public void windowClosing(WindowEvent evt){
			f.setVisible(false);
			f.dispose(); // dispose of resources
		}
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////
	//// Test
	///////////////////////////////////////////////////////////////////////////////////////////

	public String toString(){
		String s = "Plot2D:\n";
		s += "* width="+getSize().width+"\n";
		s += "* height="+getSize().height+"\n";
		s += "* ptOrg=("+ptOrg.x+", "+ptOrg.y+")\n";
		s += "* scale=("+scale.x+", "+scale.y+")\n";
		s += "* leftMarginPx: " + leftMarginPx + "\n";
		s += "* rightMarginPx: " + rightMarginPx + "\n";
		s += "* topMarginPx: " + topMarginPx + "\n";
		s += "* bottomMarginPx: " + bottomMarginPx + "\n";
		s += xAxis.toString();
		s += yAxis.toString();
		return s;
	}
	
	public static void main(String[] args){
		test1(args);
	}
		
	public static void test1(String[] args){
		JFrame frame = new JFrame("test");
		frame.setSize(400,400);
		frame.getContentPane().setLayout(new BorderLayout());

		DefaultCurve2D curveA = new DefaultCurve2D();
		DefaultCurve2D curveB = new DefaultCurve2D();
		for (double x = -23.4; x < 93; x+=1.0) {
			curveA.addXY(x, Math.random());
			curveB.addXY(x, Math.sin(x/20*Math.PI));
		}

		Plot2D plot = new Plot2D(Color.black,Color.white, Color.gray);
		plot.setForeground(Color.red); // title, keys
		plot.addCurve(curveA,"A",Color.green, DecoratedCurve2D.LINES);
		plot.addCurve(curveB, "B", Color.cyan, DecoratedCurve2D.DOTS);
		plot.setTitle("y=f(x)");
		plot.setXaxisLabel("sample");
		frame.getContentPane().add(plot,"Center");
		JButton b = new JButton("add xy");
		b.addActionListener(new ActionHandler1(curveA,plot));
		frame.getContentPane().add(b, "East");
		b=new JButton("redraw");
		b.addActionListener(new ActionHandler2(plot));
		frame.getContentPane().add(b, "West");
		frame.show();
		//new javax.swing.Timer(500, new ActionHandler(curveA,plot)).start();
	}

	static class ActionHandler1 implements ActionListener {
		DefaultCurve2D c;
		Plot2D plot;
		ActionHandler1(DefaultCurve2D c,Plot2D plot){
			this.c=c; this.plot=plot;
		}
		public void actionPerformed(ActionEvent e){
			//System.out.println("action !");
			c.addXY(c.getMax(c.X_AXIS)+1.0, Math.random());
			plot.dataChanged();
		}
	}
	static class ActionHandler2 implements ActionListener {
		Plot2D plot;
		ActionHandler2(Plot2D plot){
			this.plot=plot;
		}
		public void actionPerformed(ActionEvent e){
			//System.out.println("action !");
			plot.redraw();
		}
	}

}

