import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
/*
    
*/
/**
 *	球面上の最も離れた点はどこにあるか?
 */
public class Hello3D_Sphere6 extends Applet 
 implements MouseMotionListener, MouseListener, Runnable, ActionListener{
	
	private static final long serialVersionUID = -8931409060482821778L;
	
	ArrayList earth;	// ベースとなる球面
	ArrayList komas;	// 球面上に駒を置いてみる
	
	int N_KOMA = 7;	// 駒の数
	double CO_ANTI = 0.02;	// 反発係数
	
    Point center;         // アプレットの中心座標
    Point mousePosition;  // マウス位置
    double scale;         // モデル描画時のスケール
    double phi;           // x軸周りの回転角
    double theta;         // y軸周りの回転角
    
    Image bufferImage;    // ダブルバッファリング用のイメージ
    Dimension appletSize; // アプレットサイズ
    static final int BUTTON_MARGIN = 40;	// コンポーネント分の高さ
    
    // コンポーネント
    Canvas	theCanvas;
    TextField	txt_koma;	// 駒の数
    TextField	txt_anti;	// 反発係数
    Button	btn_run;
    
    Thread	main_th;	// 実行スレッド
	
    public void init() {
        
        // アプレットサイズの取得
        appletSize = getSize();
        
        // アプレット全体のレイアウト
    	this.setLayout(new FlowLayout());
    	
        // 描画領域の作成
        theCanvas = new Canvas();
        theCanvas.setBackground(Color.BLACK);
        theCanvas.setSize( appletSize.width, appletSize.height - BUTTON_MARGIN );
        theCanvas.addMouseMotionListener(this);
        theCanvas.addMouseListener(this);
        this.add(theCanvas);
        
        // テキスト入力 -- 駒の数
        this.add( new Label("N-Points:") );
        txt_koma = new TextField();
        txt_koma.setText( String.valueOf(N_KOMA) );
        this.add(txt_koma);
        
        // テキスト入力 -- 反発係数
        this.add( new Label("Co-Effect:") );
        txt_anti = new TextField();
        txt_anti.setText( String.valueOf(CO_ANTI) );
        this.add(txt_anti);
        
        // ボタン
        btn_run = new Button("Run");
        btn_run.addActionListener( this );
        this.add( btn_run );
        
        // アプレットの中心座標の取得
        center = new Point(appletSize.width / 2, (appletSize.height - BUTTON_MARGIN) / 2);
        
        // 描画スケールの設定
        scale = appletSize.width * 0.8 / 2; 
        
        // ダブルバッファリング用のイメージを作成
        if(bufferImage == null) {
            bufferImage = createImage(appletSize.width, (appletSize.height - BUTTON_MARGIN)); 
        }
        
        // アプリケーションの初期化
        initApp();
        
        //自分を渡してThreadクラスを作成しスレッド起動
        main_th = new Thread(this);
        main_th.start();
    }
    
    // アプリケーションの初期化
    private void initApp(){
    	
        // マウス位置の初期化
        mousePosition = new Point(0, 0);
        
        // 回転角の初期化
        phi   = 0.0;
        theta = 0.0;
        
        // モデルデータの設定
        setModelData();
         
        // 頂点のスクリーン座標の設定
        setScreenPosition();
    }
    
    public void paint(Graphics g) {
        
        // バッファにモデルを描画
        drawModel(bufferImage.getGraphics());
        
        // バッファイメージをアプレットに描画
        Graphics gc = theCanvas.getGraphics();
        gc.drawImage(bufferImage, 0, 0, this);
    }
    
    // 描画更新時に背景の塗りつぶし処理を行わないためのオーバーライド
    public void update(Graphics g) {
    	paint(g);
    }
    
    // ボタンが押されたときの処理
	public void actionPerformed( ActionEvent ev )
	{
		boolean	is_error = false;
		
		// 実行ボタン -- アプリの再実行
		if( ev.getSource() == btn_run )
		{
			// txt_koma の入力値を得る
			try{
				N_KOMA = Integer.parseInt( txt_koma.getText() );
			}
			catch(NumberFormatException ex ){
			//	ex.printStackTrace();
				txt_koma.setText( String.valueOf(N_KOMA) );
				is_error = true;
			}
			
			// txt_antiの入力値を得る
			try{
				CO_ANTI = Double.parseDouble( txt_anti.getText() );
			}
			catch(NumberFormatException ex ){
			//	ex.printStackTrace();
				txt_anti.setText( String.valueOf(CO_ANTI) );
				is_error = true;
			}
			
			if( ! is_error ){ // エラーが無ければ
				initApp();	// アプリケーションの再初期化
			}
		}
	}
	
	// マウスイベント
    public void mouseMoved(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) { }
    public void mouseEntered(MouseEvent e) { }
    public void mouseExited(MouseEvent e) { }
    public void mouseReleased(MouseEvent e) { }
    public void mouseDragged(MouseEvent e) {
        // 回転角の更新
        theta += (e.getX() - mousePosition.x) * 0.01;
        phi   += (e.getY() - mousePosition.y) * 0.01;
        
        // x軸周りの回転角に上限を設定
        phi = Math.min(phi, Math.PI/2);
        phi = Math.max(phi, -Math.PI/2);
        
        // マウス位置の更新
        mousePosition.setLocation(e.getX(), e.getY());
        
        // 頂点のスクリーン座標の更新
        setScreenPosition();
        
        // 描画更新
        repaint();
    }
    
    public void mousePressed(MouseEvent e) {
        // マウス位置の更新
        mousePosition.setLocation(e.getX(), e.getY());
    }
    
    // モデルデータの設定
    private void setModelData() {
        
        earth = new ArrayList();
        komas = new ArrayList();
        
        // ベース球面モデルの作成
		earth.add( new VertexR( 0, Math.PI ) );	// 北極
		
        for(double z = -1.0+0.25; z < 1.0; z += 0.25 ){
        	for(int i=0; i < 12; i++ ){
        		earth.add(
        			new VertexR(
        				i * 2 * Math.PI / 12 ,
        				Math.acos( z )
        			)
        		);
        	}
        }
        
		earth.add( new VertexR( 0, 0 ) );	// 南極
		
        // 駒の初期化
		for(int i=0; i < N_KOMA; i++ ){
			VertexK koma = sphereRndKoma();
			komas.add( koma );
		}
    }
    
    // 駒をランダムな位置に作成する
    private VertexK sphereRndKoma() {
    	double z = Math.random() * 2 -1;
    	double rs = Math.random() * Math.PI * 2;
    	
    	return new VertexK( rs, Math.acos(z) );
    }
    
    // 頂点のスクリーン座標を更新する
    private void setScreenPosition() {
        
        for(int i = 0; i < earth.size(); i++) {
            VertexR v = (VertexR)earth.get(i);
            v.rotate(theta, phi);	// 回転後の座標値の算出
            v.project(center, scale);	// スクリーン座標の算出
        }
        
        for(int i = 0; i < komas.size(); i++) {
        	VertexK koma = (VertexK)komas.get(i);
        	koma.rotate(theta, phi);
        	koma.project(center, scale);
        }
    }
    
    // モデルの描画
    private void drawModel(Graphics g) {
    	
    	final int KOMA_SIZE = 4;
    	
    	// 全体をクリア
    	g.setColor(Color.BLACK);
    	g.fillRect(0, 0, appletSize.width, appletSize.height);
    	
    	// ベース球面の描画
    	g.setColor(Color.GREEN);
    	drawEarth(g);
    	
    	// 駒の描画
        for(int i = 0; i < komas.size(); i++) {
        	VertexK koma = (VertexK)komas.get(i);
        	
        	if( koma.ry >= 0.0 ){	// 手前と奥で色を変える
        		g.setColor(Color.YELLOW);
        	}else{
        		g.setColor(Color.BLUE);
        	}
        	
        	g.fillOval(koma.screenX - KOMA_SIZE, koma.screenY - KOMA_SIZE, 2 * KOMA_SIZE, 2 * KOMA_SIZE);
        }
    }
    
    // ベース球面の描画 -- 頂点をうまい具合に線で結ぶ
    private void drawEarth(Graphics g) {
    	
        VertexR v1, v2;
        int i;
        int base = 1;
        
        // 経線(横線)を描く
    //	for( double z = -1.0+0.25; z < 1.0; z += 0.25 ){
        for( int j= -3; j < 4; j++ ){
        	for(i=0; i < 12 -1; i++ ){
        		v1 = (VertexR)earth.get(base + i);
        		v2 = (VertexR)earth.get(base + i + 1); // 1つ先の頂点と結ぶ
        		g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY );
        	}
        	// 円の最後を閉じる
        	v1 = (VertexR)earth.get(base + i);
        	v2 = (VertexR)earth.get(base);
        	g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY );
        	base += (++i);
        }
        
        // 緯線(縦線)の北極回り
        base = 0;
        v1 = (VertexR)earth.get(base);
        base = 1;
    	for(i=0; i < 12; i++ ){
    		v2 = (VertexR)earth.get(base + i);
        	g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY );
    	}
    	
    	// 緯線を描く
    	base = 1;
    //	for( double z = -1.0+0.25; z < 1.0-0.25; z += 0.25 ){
        for( int j= -3; j < 3; j++ ){
        	for(i=0; i < 12; i++ ){
        		v1 = (VertexR)earth.get(base + i);
        		v2 = (VertexR)earth.get(base + i + 12); // 12個先の頂点と結ぶ
            	g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY );
        	}
        	base += i;
        }
        
        // 緯線(縦線)の南極回り
        v2 = (VertexR)earth.get( earth.size() - 1 );
    	for(i=0; i < 12; i++ ){
    		v1 = (VertexR)earth.get(base + i);
        	g.drawLine( v1.screenX, v1.screenY, v2.screenX, v2.screenY );
    	}
    }
    
    public void destroy() {
    	main_th = null;	//スレッドの終了
    }
    
    public void	run()
    {
    	while(main_th != null){
    		
    		// 駒を動かす
    		moveKomas();
    		
    		// 頂点のスクリーン座標の更新
    		setScreenPosition();
    		
			repaint(); // 再描画
			
			try	// 時間待ち
			{
				Thread.sleep(50);
			}
			catch( InterruptedException ex )
			{
				ex.printStackTrace();
			}
    	}
    }
    
    // 駒を動かす
    private void moveKomas(){
    	
    	// 自分以外の全ての相手との相互作用を得る
        for(int i = 0; i < komas.size(); i++) {
        	VertexK koma1 = (VertexK)komas.get(i);
        	
        	// koma1を北極にもっていく角度
        	double rot_th = koma1.th - Math.PI/2;
        	double rot_ph = - koma1.ph;
        	
        	double sum_x = 0.0, sum_y = 0.0;
        	
        	for(int j = 0; j < komas.size(); j++) {
        		if( i == j ){ // 自分自身との相互作用は無い
        			continue;
        		}
        		
        		VertexK koma2 = (VertexK)komas.get(j);
        		
        		double save_x = koma2.x;	// 現状を保存
        		double save_y = koma2.y;
        		double save_z = koma2.z;
        		double save_th = koma2.th;
        		double save_ph = koma2.ph;
        		
        		// koma1を北極にもっていったとき、koma2はこの位置にくる
        		koma2.rotate( rot_th, rot_ph );
        			// x, y, z -> rx, ry, rz
        		koma2.x = koma2.rx;
        		koma2.y = koma2.ry;
        		koma2.z = koma2.rz;
        		koma2.DtoR();
        		
        		// koma2への変異を足し合わせる
        		double r;
        		if( koma2.ph > 0.00001 ){ // 0割りを防ぐ
        			r = 1 / koma2.ph;	// 角度の逆数を影響力と見なす
        		}else{
        			r = 1 / 0.00001;
        		}
        		sum_x += r * Math.cos( koma2.th );
        		sum_y += r * Math.sin( koma2.th );
        		
        		koma2.x = save_x;	// 現状を復帰
        		koma2.y = save_y;
        		koma2.z = save_z;
        		koma2.th = save_th;
        		koma2.ph = save_ph;
        	}
        	
        	koma1.dx = - sum_x;
        	koma1.dy = - sum_y;
        		// 斥力だからマイナス
        }
        
        // 相互作用に従って koma1 を移動する
        for(int i = 0; i < komas.size(); i++) {
        	VertexK koma1 = (VertexK)komas.get(i);
        	
        	// koma1を北極にもっていく角度
        	double rot_th = koma1.th - Math.PI/2;
        	double rot_ph = - koma1.ph;
        	
        	double sum_x = koma1.dx;
        	double sum_y = koma1.dy;
        	
        //	koma1.rotate( rot_th, rot_ph );	// 回転して北極に持ってくる
        		// x, y, z -> rx, ry, rz
        //	koma1.x = 0.0;	// koma1.rx;
        //	koma1.y = 0.0;	// koma1.ry;
        //	koma1.z = 1.0;	// koma1.rz;
        		// 実際に北極に持ってゆく計算は不要、どうせ上書きするので。
        	
        	double r2 = sum_x * sum_x + sum_y * sum_y;
        	
        	koma1.th = VertexK.atanXY( sum_x, sum_y );
        	koma1.ph = CO_ANTI * r2;
        	koma1.RtoD();
        		// th, ph -> x, y, z
        	
        	koma1.rotate2( rot_th, rot_ph );	// 北極で微小移動した駒を元に戻す
        		// x, y, z -> rx, ry, rz
        	koma1.x = koma1.rx;
        	koma1.y = koma1.ry;
        	koma1.z = koma1.rz;
        	
        	koma1.DtoR();
        		// x, y, z -> th, ph
        }
    }
}
/**
 *	運動する頂点クラス
 */
class VertexK extends VertexR {
	
	public double dx, dy;	// 位置の変異 = 運動
	
	public VertexK(double th, double ph) {
		super(th, ph);
	}
	
	public VertexK(double x,double y,double z) {
		super(x,y,z);
	}
}
/**
*	頂点クラス
*/
class VertexR {
	public double th, ph;        // モデルの頂点角度座標
	public double x, y, z;       // モデルの頂点座標
	public double rx, ry, rz;    // 回転させた後の座標
	public int screenX, screenY; // スクリーン上の座標
    
    // コンストラクター、角度から
    public VertexR(double th, double ph) {
        this.th = th;
        this.ph = ph;
        RtoD();
    }
    
    // 角度 -> XYZ
    public void RtoD(){
        this.x = Math.cos(this.th) * Math.sin(this.ph);
        this.y = Math.sin(this.th) * Math.sin(this.ph);
        this.z = Math.cos(this.ph);
    }
    
    // コンストラクター、XYZから
    public VertexR(double x,double y,double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    // 回転する
    public void rotate(double theta, double phi) {
        // 回転後の座標値の算出
        this.rx = 
        		+ this.x * Math.cos(theta)
        		+ this.y * Math.sin(theta);
        this.ry = 
        		- this.x * Math.cos(phi) * Math.sin(theta) 
        		+ this.y * Math.cos(phi) * Math.cos(theta)
        		+ this.z * Math.sin(phi);
        this.rz = 
        		+ this.x * Math.sin(phi) * Math.sin(theta) 
        		- this.y * Math.sin(phi) * Math.cos(theta)
        		+ this.z * Math.cos(phi);
    }
    
/* -- 回転変換
    
    X軸回りに-φ回転  ○  Z軸回りに-θ回転  = 合成
    
    1      0       0      cosθ   sinθ  0    cosθ            sinθ     0
    0   cosφ  sinφ     -sinθ   cosθ  0   -cosφsinθ  cosφcosθ  sinθ
    0  -sinφ  cosφ          0      0   1    sinφsinθ -sinφcosθ  cosφ
*/
    
/* -- 逆変換
    
    Z軸回りにθ回転  ○  X軸回りにφ回転    = 合成
    
    cosθ -sinθ 0       1     0       0      cosθ  -sinθcosφ  sinθsinφ
    sinθ  cosθ 0       0  cosφ  -sinφ     sinθ   cosθcosφ -cosθsinφ
       0      0  1       0  sinφ   cosφ     0       sinφ      cosφ
*/
    // 逆回転する
    public void rotate2(double theta, double phi) {
    	// 回転後の座標値の算出
    	this.rx = 
    		+ this.x * Math.cos(theta)
    		- this.y * Math.sin(theta) * Math.cos(phi)
    		+ this.z * Math.sin(theta) * Math.sin(phi);
    	this.ry = 
    		+ this.x * Math.sin(theta)
    		+ this.y * Math.cos(theta) * Math.cos(phi)
    		- this.z * Math.cos(theta) * Math.sin(phi);
    	this.rz = 
    		+ this.y * Math.sin(phi)
    		+ this.z * Math.cos(phi);
    }
    
    // XYZ->角度
    public void DtoR(){
    	this.ph = Math.acos(this.z);
    	this.th = atanXY(this.x, this.y);
    }
    
    // X,Y 座標から角度を逆算する
    public static double atanXY(double x0, double y0) {
    	
    	double retval;
    	
    	if( x0 == 0 && y0 == 0 ){ // 0割を防ぐ
    		return 0.0;
    	}
    	
    	if( y0 <= x0 ){
        	if( y0 > - Math.abs(x0) ){
        		retval = Math.atan( y0 / x0 );
        	}
        	else{
        		retval = Math.PI / 2 - Math.atan( x0 / y0 ) + Math.PI;
        	}
        }
        else{ // this.y > this.x
        	if( y0 > Math.abs(x0) ){
        		retval = Math.PI / 2 - Math.atan( x0 / y0 );
        	}
        	else{
        		retval = Math.atan( y0 / x0 ) + Math.PI;
        	}
        }
    	return retval;
    }
    
    // スクリーン上に投影する
    //	X軸が横、Z軸が縦、Y軸を奥行きとしている
    //	通常の、Z軸が奥行きという座標系とは異なっているので注意
    public void project(Point center, double scale) {
    	this.screenX = (int)(center.x + scale * this.rx );
    	this.screenY = (int)(center.y - scale * this.rz );	// <- ここが rz
    }
}