//
//  GraphPlot.java  -- graph drawing class for Typodrome
//
//                     by Javva Brothers <jb@absurd.org>
//
//                     url: <http://www.absurd.org/absurd/typodrome>
//
// This Program Is A Public Domain. It may be freely distributed in source or
// binary for without any restrictions. Author release all the rights on
// this program and disclaim any responsibility for any damages suffered 
// by the users of this program. 
//


import java.applet.*;
import java.awt.*;
import java.util.*;

//-----------------------------------------------------------------------
//
//  GraphPlot
//
//-----------------------------------------------------------------------

public class GraphPlot extends Canvas
{
        private Vector data_;
        
        private String name_;
        
        private int xmax_scale_;      // max. elements on the X scale
        private int y_range_max_;    
        private int y_range_min_;
        private int y_range_step_;     // number of elements to increment y scale
        private int x_range_start_;
        private int x_range_end_;
        private int x_range_shift_;
        
        private int x0_;        // graph origin
        private int y0_;
        private int x1_;
        private int y1_;
        
        public boolean has_xmarks_;
        
        private int gap_;       // between frame and graph
        
        // font metrics
        // this are necessary for the inner class, and since we don't have friends
        // they declared public
        public int adv_;
        public int asc_;
        public int desc_;
        
        private FontMetrics fm_;
        private Vector xmarks_;
        private Vector ymarks_;
                
        GraphPlot( String name, 
                int ymax, int ymin, int yscalestep, 
                int xstep, boolean has_xmarks )
        {
                data_ = new Vector();
                name_ = new String(name);
                has_xmarks_ = has_xmarks;
                
                gap_ = 5;
                
                x0_ = y0_ = -1;  
                x_range_start_ = 0;
                x_range_end_ = xstep;              
                
                y_range_max_ = ymax;
                y_range_min_ = ymin;
                y_range_step_ = yscalestep;
        }
        
        // oddly enough, wihout implementation of this method
        // paint() WILL NOT BE CALLED !!!!
        public Dimension preferredSize() 
        {
                return new Dimension(size().width, size().height);  // rely on parent
        }
        
        private void measure()
        {
                if( fm_ == null )
                {        
                        Font font = new Font( "Helvetica", Font.PLAIN, 10 );
                        setFont(font);
                        fm_ = getFontMetrics(font);
                        adv_ = fm_.getMaxAdvance();
                        asc_ = fm_.getMaxAscent();
                        desc_ = fm_.getMaxDescent();
                        measure_y_scale();
                        measure_x_scale();
                        xmarks_ = ymarks_ = null;
                }
                calculate_marks();
        }
        
        private void measure_x_scale()
        {
                // we assume we don't need numbers larger than 1000
                int w = fm_.stringWidth(new String( "0000" ));
                x0_ = gap_ + w + gap_;
                x1_ = size().width - 2 * gap_;
        }
        
        private void measure_y_scale()
        {
                int height = size().height;
                int h2 = (asc_ + desc_) / 2; // font half-height
                
                y0_ = height - h2;
                if( has_xmarks_ )                           
                        y0_ -= (asc_ + desc_ + gap_);
                y1_ = gap_ + asc_ + desc_ + h2;  // height of the name string + gap
        }
        
        private void calculate_marks()
        {
                if( xmarks_ == null )
                        xmarks_ = calculate_grid( x_range_end_, x_range_start_, 
                                                        x1_, x0_, adv_*3 );
                if( xmarks_.size() > 1 )
                        x_range_shift_ = 
                                ((Integer)(xmarks_.elementAt(5))).intValue() -
                                ((Integer)(xmarks_.elementAt(2))).intValue();
                else
                        x_range_shift_ = 0;
                        
                if( ymarks_ == null )
                        ymarks_ = calculate_grid( y_range_max_, y_range_min_, 
                                                        y1_, y0_, asc_ + desc_ );
        }
        
        private Vector calculate_grid( int max_val, int min_val, 
                                        int max_coo, int min_coo, int lbl_ext )
        {
                int range = max_val - min_val;
                int d = max_coo - min_coo;      // always positive
                if( d < 0 )
                        d = -d;
                int div[] =    {  1, 5, 10, 20, 25, 50, 100, -1 };

                
                // we need to get 5-15 lines with min. distance between the
                // lines 10 pixels
                int n = 10;     // initial number of lines
                int d_coo = d / n;
                if( d_coo < 10 )
                        n = d / 10;     // means 10 pixels per line
                if( n <= 0 )
                        n = 1;
                int range_inc = range / n;

                // now find the closest increment
                // alays go with the biggest available, i.e. less lines is better
                //
                for( int i = 0; div[i] > 0; i++ )
                        if( div[i] >= range_inc || div[i+1] < 0 )
                        {
                                range_inc = div[i];
                                break;
                        }
                
                // adjust d back to normal -- in case of Y axis it 
                // should be negative
                int dir = 1;
                if( max_coo < min_coo )
                {
                        d = -d;
                        dir = -1;
                        lbl_ext = -lbl_ext;
                }
                int lbl_x05 = lbl_ext / 2;

                // now fill up resulting vector
                Vector v = new Vector();
                int low_bound = min_coo + lbl_ext/2;
                int hi_bound = max_coo - lbl_ext/2;
                for( int val = min_val+range_inc; val < max_val; val += range_inc )
                {
                        // always calculate coordinate by scaling the real value
                        // 
                        int coo = min_coo + (((val-min_val) * d) / range);
                        boolean is_lbl = true;
                        
                        if( ((coo - lbl_x05 - low_bound) * dir) < 0 ||
                                        ((coo + lbl_x05 - hi_bound) * dir) > 0 )
                               is_lbl = false;
                        else
                                low_bound = coo + lbl_x05;
                                
                        add_marker( v, coo, is_lbl, val);
                } 
                
                return v;
        }
        
        private void add_marker( Vector v, int coo, boolean is_lbl, int val )
        {
                v.addElement( new Integer(coo) );
                v.addElement( new Boolean(is_lbl) );
                v.addElement( new Integer(val) );
        }
        
        public void paint( Graphics g )
        {
                measure();
                draw_name(g);
                draw_x_axis(g);
                draw_y_axis(g);
                draw_segments(g);
        }
        
        private boolean adjust_x_range()
        {
                int d_range = x_range_shift_;
                if( data_.size() > (x_range_end_ - x_range_start_ + 1) )
                {
                        x_range_start_ += d_range;
                        x_range_end_ += d_range;
                        while( d_range-- > 0 )
                                data_.removeElementAt(0);
                        xmarks_ = null;
                        return true;
                }
                else
                        return false;
        }
        
        private boolean adjust_y_range( int value )
        {
                if( value > y_range_max_ )
                {
                        while( value > y_range_max_ )
                                y_range_max_ += y_range_step_;
                        ymarks_ = null;
                        return true;
                }
                else if( value < y_range_min_ )
                {
                        while( value < y_range_min_ )
                                y_range_min_ -= y_range_step_;
                        ymarks_ = null;
                        return true;
                }
                else
                        return false;
        }
        
        
        
        public void addTick( int value )
        {

                data_.addElement( new Integer(value) );
                boolean do_repaint = adjust_y_range( value );
                
                do_repaint = (do_repaint || adjust_x_range());
                
                if( do_repaint )
                        repaint();
                else
                        draw_segment( getGraphics(), data_.size()-1 );
        }

        private void draw_segments( Graphics g )
        {
                for( int i = 1; i < data_.size(); i++ )
                        draw_segment( g, i );
        }
        
        private void draw_segment( Graphics g, int idx )
        {
                if( idx <= 0 || idx >= data_.size() )
                        return;
                        
                g.setColor( Color.blue );
                Point pt0 = get_point_at( idx - 1 );                
                Point pt1 = get_point_at( idx );
                g.drawLine( pt0.x, pt0.y, pt1.x, pt1.y );
        }

        private Point get_point_at( int idx )
        {
                int x = x0_ + ((idx * (x1_-x0_)) / 
                                                (x_range_end_ - x_range_start_));
             
                int val = ((Integer)(data_.elementAt(idx))).intValue();
                int y = y0_ + (((val - y_range_min_) * (y1_-y0_)) / 
                                                (y_range_max_ - y_range_min_));
                return new Point(x, y);                                                
        }

        private void draw_name( Graphics g )
        {
                g.setColor( Color.black );
                g.setFont(getFont());
                g.drawString( name_, gap_, gap_+asc_ );
        }

        private void draw_x_axis( Graphics g )
        {
                g.setColor( Color.red );
                g.drawLine( x0_, y0_, x1_, y0_ ); 
                g.setColor( Color.gray );
                g.drawLine( x0_, y1_, x1_, y1_ ); 
                                
                g.setColor( Color.black );
                int y = y0_ + gap_ + asc_;
                g.drawString( String.valueOf(x_range_start_), x0_, y);
                String str = String.valueOf( x_range_end_);
                int dw = fm_.stringWidth(str);
                g.drawString( str, x1_-dw, y );

                for( int i = 0; i < xmarks_.size(); i += 3 )
                        draw_vert( g, y0_, y1_, i );


        }

        private void draw_y_axis( Graphics g )
        {
                int height = size().height;
                g.setColor( Color.red );
                g.drawLine( x0_, y1_, x0_, y0_ ); 
                g.setColor( Color.gray );
                g.drawLine( x1_, y1_, x1_, y0_ ); 
                                                
                g.setColor( Color.black );
                draw_y_mark( g, y_range_max_, y1_ );
                draw_y_mark( g, y_range_min_, y0_ );
                
                for( int i = 0; i < ymarks_.size(); i += 3 )
                        draw_horiz( g, x0_, x1_, i );
        }
        
        // to make these methods accessible from the nested class Marker,
        // we forced to make this method public
        //

        // aligning at the CENTER !
        public void draw_x_mark( Graphics g, int n, int x )
        {
                String str = String.valueOf(n);
                int dw = fm_.stringWidth(str);
                g.drawString( str, x - dw/2, y0_ + gap_ + asc_ );
        }

        public void draw_y_mark( Graphics g, int n, int y )
        {
                String str = String.valueOf(n);
                int dw = fm_.stringWidth(str);
                g.drawString( str, x0_-dw-gap_, y + ((asc_-desc_) / 2) );
        }
        
        public void draw_horiz( Graphics g, int x0, int x1, int idx )
        {
                int coo = ((Integer)(ymarks_.elementAt(idx++))).intValue();
                boolean has_label = 
                                ((Boolean)(ymarks_.elementAt(idx++))).booleanValue();
                int val = ((Integer)(ymarks_.elementAt(idx))).intValue();
                
                g.setColor( Color.lightGray );
                g.drawLine( x0, coo, x1, coo );
                if( has_label )
                {
                        g.setColor( Color.darkGray );
                        draw_y_mark( g, val, coo );
                }
        }
                
        public void draw_vert( Graphics g, int y0, int y1, int idx )
        {
                int coo = ((Integer)(xmarks_.elementAt(idx++))).intValue();
                boolean has_label = 
                                ((Boolean)(xmarks_.elementAt(idx++))).booleanValue();
                int val = ((Integer)(xmarks_.elementAt(idx))).intValue();
                
                g.setColor( Color.lightGray );
                g.drawLine( coo, y0, coo, y1 );
                if( has_label && has_xmarks_ )
                {
                        g.setColor( Color.darkGray );
                        draw_x_mark( g, val, coo );
                }
        }
        
}

