
/**
 * Own.java    : 1-Wire Sensors Interface and Graph plotter
 * Author      : Device Drivers, Ltd.
 * URL         : http://www.devdrv.co.jp/
 * Date        : 04/26/2006
 */
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;

/*
    <APPLET CODE="Own.class" WIDTH=600 HEIGHT=600>
    <PARAM NAME="TimeHour", VALUE="12">
    <PARAM NAME="DebugLevel", VALUE="1">
    </APPLET>
*/

class Config { //Applet全体の設定とデバッグ
	public static int level = 0; //デバッグ用
	public static boolean localtest = false;
	private static int timeHour = 6; //デフォルトは6時間単位で表示
	public static int level() { return level; } //デバッグレベル
	public static void setLevel(int i) { if (i >= 0) level = i; }
	public static int th() { return timeHour; }                  //表示単位: 1,2,3,4,6,8,12...
	public static void setTh(int h) { if (h > 0) timeHour = h; } //日付をまたぐ場合にずれるので
}                                                                   //24を割り切れる値を推奨

class Recorder { //ファイルへのデータ記録
	private static FileWriter f = null;
	private static String recorderDir = null;
	private static String recorderFile = null;
	private static boolean fileop = false;

	public Recorder(String dir, String file) {
		if (file == null) fileop = false;
		else { recorderDir = dir; recorderFile = file; }
	}
	public Recorder() {}
	public String fileName() { //記録先のファイル名
		return(recorderFile == null ? null : recorderDir + recorderFile);
	}
	public boolean isFile() { //記録先がファイルかどうかの問い合わせ
		return(fileop);
	}
	public boolean setFile() { //記録先をファイルにするかのトグル
		return(fileop = !fileop);
	}
	public void setFile(boolean file) { //file=1の場合、記録先
		fileop = file;
	}
	public void store(List<StringBuffer> list) {
		try { //リストを全件出力
			FileWriter lf = new FileWriter(new File(recorderDir, recorderFile));
			for(StringBuffer b : list)
				lf.write(b + "\r\n");
			lf.close();
		}
		catch(Exception e){
			System.out.println("Exception-store : " + e);
		}
	}
	public void add(String s) {
		if (fileop && f != null) //ファイル出力有効時だけ動作
			try {
				if (Config.level() > 1) System.out.println("r.add : " + s);
				f.write(s + "\r\n");
			}
			catch(Exception e) {
				System.out.println("Exception-add : " + e);
			}
	}
	public void open() {
		close();
		if (fileop)
			try {
				f = new FileWriter(new File(recorderDir, recorderFile), true);
			}
			catch(IOException e){
				System.out.println("Exception-open : " + e);
			}
	}
	public void close() {
		if (f != null)
			try { f.close(); }
			catch (IOException ie) {}
			f = null;
	}
	public void finalize() { close(); }
}

class SensorManager { //接続しているセンサ情報の管理
	public int index;
	public int type;
	public int interval; //何秒ごとにデータ取得するか
	public long limitMills; //データ取得サイクル算出用
	public long serial;
	public StringBuffer name;

	public SensorManager(int id, int t, int iv, long ser, String n)
		throws IndexOutOfBoundsException {
		if (id >= 0 && id < 10) {
			index = id;
			type = t;
			interval = iv;
			serial = ser;
			name = new StringBuffer(n);
		}
		else {
			String s = String.format("範囲外のindex = %d", id);
			throw new IndexOutOfBoundsException(s);
		}
	}
	public void setLimit(long currentMills) {
		limitMills = currentMills + interval * 1000;
	}
}

class SecondSummary { //秒単位データ集計（データ集計の下位クラス）
	public int second;
	public double data;
	public SecondSummary(int s, double d) {
		second = s; data = d;
	}
}

class DataSummary { //データ集計
	public SecondSummary sec[];
	public int dx[];
	public int dy[];
	public double low;
	public double high;
	public int width;
	public int height;
	public SensorManager sm;
	public int sIndex;
	private int dIndex;

	public DataSummary(int l, int h, int wd, int ht, SensorManager m) {
		low = (double) l; high = (double) h;
		width = wd; height = ht;
		sec = new SecondSummary[Config.th()*4*60*60]; //全集計範囲は単位時間の4倍（固定）
		dx = new int[wd];
		dy = new int[wd];
		sm = m;
		sIndex = 0;
	}
	public void add(int i, double d) {
		sec[sIndex] = new SecondSummary(i, d);
		sIndex++;
	}
	public int dotCount() { return dIndex; }
	public void summary(int xOffset, int yOffset) {
	    int count;
	    int index;
	    double sum;
	    double mean, major;
	    double lastmean;
	    double plusmajor;
	    double minusmajor;

	    if (Config.level() > 2) { System.out.println("summary = " + sIndex); }
	    dIndex = 0;
	    lastmean = Double.NEGATIVE_INFINITY;
	    for(int i = 0; i < width; i++) {
	    	count = 0;
	    	sum = 0;
	    	plusmajor = Double.MIN_VALUE; //最小の代表値算出のための初期値
	    	minusmajor = Double.MAX_VALUE; //最大の代表値算出のための初期値

	    	for(SecondSummary ss : sec) { //記録されている秒数を読んでプロットする。
	    		if (ss == null) continue;
	    		index = ss.second / (Config.th()*4*60*60 / width);
	    		if (index == i) {
	    			count++;
	    			sum += ss.data; //各+-値で、最も顕著な値を代表値候補とする
	    			if (ss.data > plusmajor) plusmajor = ss.data;
	    			if (ss.data < minusmajor) minusmajor = ss.data;
	    		}
	    	} //for
	    	if (count == 0) { //該当データが無いのでプロットしない
	    		lastmean = Double.NEGATIVE_INFINITY;
	    		continue;
	    	}
	    	mean = sum / count;
	    	if (mean > high || mean < low) { //プロット範囲外の値
	    		System.out.println("Invalid range index = " + i + ", data = " + mean);
	    		lastmean = Double.NEGATIVE_INFINITY;
	    		continue;
	    	}
	    	dx[dIndex] = i + xOffset;
	    	if (lastmean == Double.NEGATIVE_INFINITY) { //前回データ無し
	    		dy[dIndex] = (int) (height - mean * (height / (high - low))) + yOffset;
	    		lastmean = mean;
	    	}
	    	else { //前回データがある場合：今回の代表値を求める
	    		if (mean > lastmean) { //平均が前回よりも大きい場合
	    			major = plusmajor; //最大値を代表値とする
	    		}
	    		else if (mean < lastmean) { //平均が前回よりも小さい場合
	    			major = minusmajor;      //最小値を代表値とする
	    		}
	    		else {
	    			major = mean; //平均値をそのまま使う
	    		}
    			dy[dIndex] = (int) (height - major * (height/(high-low))) + yOffset;
    			lastmean = major;
	    	}
	    	if (Config.level() > 2) //デバッグ：プロットデータの確認
	    		System.out.println("sum: dix=" + dIndex + ", i=" + i
	    				+ ", count=" + count + ", x="
	    				+ dx[dIndex] + ", y=" + dy[dIndex]);
	    	dIndex++;
	    } //for
	} //public
}

public class Own extends Applet implements Runnable, ActionListener
{
	/**
	 * Own -- 1-wireセンサ用インタフェイスとグラフ作成Appletのメイン処理部
	 */
	private static final long serialVersionUID = 1L;
	Button btStart, btStop, btClear, btLoad, btSave, btFile, btOK, btCancel;
	Thread timer = null;
	Thread receiver = null;
	String message = null;
	String status = null;
	String media = null;
	FileDialog fdLoad;
	FileDialog fdSave;
	Dialog dConfirm;
	Color messageColor = null;
	Calendar targetTime = Calendar.getInstance();
	Calendar currentTime = Calendar.getInstance();
	boolean recordToFile = false;
	List<StringBuffer> arrayList = new ArrayList<StringBuffer>();
	Recorder recorder;
	Semaphore sem = new Semaphore(1, true);
	Image buffer;
	Graphics g;
	boolean graphControl = true;

	//XPort Interfaces
	InetAddress xport_ip = null;
	int port = 10001;
	Socket xport_socket;
	BufferedReader socketin;
	String receive = null;
	String hostName;
	static SensorManager sMgr[];
	static final int TYPE=0, SERIAL=1, NAME=2, INDEX=3, INTERVAL=4;

	class DialogListener extends WindowAdapter {
    		public void windowClosing(WindowEvent e) {
			dConfirm.setVisible(false);
    		}
	}

	public void init() {
		long currentInMillis;
		long modInMillis;
		long targetInMillis;
		URL ownURL = null;
		BufferedReader in = null;
		String line;
		
		Dimension d = getSize();
		Dimension dim = getToolkit().getScreenSize();
		Frame fLoad = new Frame();
		Frame fSave = new Frame();
		Frame fConfirm = new Frame();
		fdLoad = new FileDialog(fLoad, "ファイルを開く", FileDialog.LOAD);
		fdSave = new FileDialog(fSave, "名前をつけて保存", FileDialog.SAVE);
		dConfirm = new Dialog(fConfirm, "確認", true);
		
		dConfirm.setSize(160, 100);
		dConfirm.setLayout(new FlowLayout());
		dConfirm.setResizable(false);
		dConfirm.add(new Label("記録データを削除します"));
		dConfirm.setLocation((dim.width-160)/2, (dim.height-100)/2);
		buffer = createImage(d.width, d.height);
		g = buffer.getGraphics();

		try {
			Config.setTh(Integer.parseInt(getParameter("TimeHour")));
			Config.setLevel(Integer.parseInt(getParameter("DebugLevel")));
		}
		catch(java.lang.NumberFormatException e) {}

		sMgr = new SensorManager[10];
		try { //Connect to XPort
			hostName = getCodeBase().getHost();
			if (Config.localtest) {
				//ownURL = new URL("http://localhost/own.txt");
				ownURL = new URL("http://rema/own.txt");
				xport_ip = InetAddress.getByName("192.168.51.132");
			}
			else {
				ownURL = new URL("http://" + getCodeBase().getHost() + "/own.txt");
				xport_ip = InetAddress.getByName(getCodeBase().getHost());			
			}
			in = new BufferedReader(new InputStreamReader(ownURL.openStream()));
			xport_socket = new Socket(xport_ip, port);
			socketin = new BufferedReader(
					new InputStreamReader(xport_socket.getInputStream()));
		}
		catch(Exception e) { System.out.println("Exception-init : " + e); }
		if (Config.level() > 0) { System.out.println("hostName = [" + hostName + "]"); }
	
		try { //own.txtファイルからセンサ管理情報の読み出し
			int i = 0;
			while((line = in.readLine()) != null) {
				if (Config.level() > 0) System.out.println("<" + line + ">");
				if (line.length() > 0 && line.charAt(0) != '#') {
					String[] params = line.split("\\s+");
					if (Config.level() > 0) {
						System.out.println("TYPE     = " + params[TYPE]);
						System.out.println("SERIAL   = " + params[SERIAL]);
						System.out.println("NAME     = " + params[NAME]);
						System.out.println("INDEX    = " + params[INDEX]);
						System.out.println("INTERVAL = " + params[INTERVAL]);
					}
					try {
						sMgr[i] = new SensorManager(
						    Integer.parseInt(params[INDEX]),
						    Integer.parseInt(params[TYPE], 16),
						    Integer.parseInt(params[INTERVAL]),
						    Long.parseLong(params[SERIAL], 16),
						    params[NAME]);
					}
					catch(NumberFormatException ne) {
					    System.out.println("不正な設定ファイル: " + line);
					    continue;
					}
					if (i >= 8) break;
					i++;
				}
			}
		}
		catch(IOException e) { System.out.println("Exception-readLine : " + e); }

		// GUI：ボタン類の初期化
		dConfirm.addWindowListener(new DialogListener());
		btStart = new Button("Start");
		btStart.addActionListener(this);
		add(btStart);

		btStop = new Button("Stop");
		btStop.addActionListener(this);
		add(btStop);

		btClear = new Button("Clear");
		btClear.addActionListener(this);
		add(btClear);

		btLoad = new Button("Load");
		btLoad.addActionListener(this);
		add(btLoad);

		btSave = new Button("Save");
		btSave.addActionListener(this);
		add(btSave);

		btFile = new Button("File");
		btFile.addActionListener(this);
		add(btFile);

		btStop.setEnabled(false);
		status = "停止：";
		media = "データ記録先：（メモリ）";
		
		btOK = (Button) dConfirm.add(new Button("OK"));
		btCancel = (Button) dConfirm.add(new Button("Cancel"));
		btOK.addActionListener(this);
		btCancel.addActionListener(this);

		// 起動時に空のグラフ枠を表示するために必要
		currentTime = Calendar.getInstance();
		currentInMillis = currentTime.getTimeInMillis();
		long offset = 9 * 60 * 60 * 1000; //GMT→JST変換用オフセット
		modInMillis = (currentInMillis + offset) % (Config.th()*60*60*1000);
		targetInMillis = currentInMillis - modInMillis - (Config.th()*3*60*60*1000);
		targetTime.setTimeInMillis(targetInMillis);

		recorder = new Recorder();
	}
	
	public void start() { if (Config.level() > 0) { System.out.println("start()"); }}
	
	public void stop() { //最小化時も動作する
		if (Config.level() > 0) { System.out.println("stop()"); }
		for (SensorManager m : sMgr)
		    if (m != null && m.type > 0) { //デバッグ：最小化時に表示
		    	if (Config.level() > 0)
		    		System.out.println(m.index + ":" + m.type + " " + m.serial);
		    }
	}
	
	public void run() {
		long currentInMillis;
		long modInMillis;
		long targetInMillis;
		String sCT;

		Thread me = Thread.currentThread();

		if (Config.level() > 1) { System.out.println("** Running: " + me.getName()); }

		if (me == timer) while (timer != null) {
			try {
				Thread.sleep(1000); //毎秒、時間とグラフスケールの更新
			}
			catch(InterruptedException e) {
				System.out.println("InterruptedException : " + e);
			}
			repaint(); // 定期的な再表示の指示
		}
		else if (me == receiver) while (timer != null) {
			try {
				receive = socketin.readLine();
			}
			catch(IOException e) {
				System.out.println("Exception-Receiver : " + e);
			}
			if (Config.level() > 2)
		  	    System.out.println("<" + receive + ">");

			try { // analyze()にcpu時間を与えるために少しsleep()
				Thread.sleep(500);
			}
			catch(InterruptedException e) {
				System.out.println("InterruptedException : " + e);
			}

			// MilliSec現在時間を取得、グラフのスケール用targetTimeを設定
			currentTime = Calendar.getInstance();
			currentInMillis = currentTime.getTimeInMillis();
			long offset = 9 * 60 * 60 * 1000;
			modInMillis = (currentInMillis + offset) % (Config.th()*60*60*1000);
			targetInMillis = currentInMillis - modInMillis - (Config.th()*3*60*60*1000);
			targetTime.setTimeInMillis(targetInMillis);

			for (SensorManager m : sMgr) {
				try {
					if (receive.length() < 16) break; //データレコードが短い場合は無視
					if (m != null
							&& m.serial == Long.parseLong(receive.substring(2, 14), 16)) {
						if (m.limitMills > currentInMillis) {
							if (Config.level() > 1)
								System.out.println("Skip record: " + m.serial);
							break; //記録対象外データ（データ取得サイクル外）
						}
						m.setLimit(currentInMillis); //データ取得サイクル用カウンタのリセット
						sCT = new String(currentInMillis + " " + receive);
						recorder.add(sCT);
						if (sem.tryAcquire(1)) {
							arrayList.add(new StringBuffer(sCT)); //取得データをリストに追加
							sem.release();
						}
						if (Config.level() > 2) { System.out.println("{" + sCT + "}"); }
						break;
					}
				}
				catch(NumberFormatException ne) {
					System.out.println("不正なデータ: " + receive);
					continue;
				}
			}
		}
		if (Config.level() > 0) { System.out.println("** Exitting: " + me.getName()); }
	}

    public void paint(Graphics gs) {
    	Dimension d = getSize();
    	Font CurrentFont = g.getFont();
    	final Font font = new Font("ＭＳ ゴシック" , Font.BOLD , 16);
    	Font statusFont = new Font("ＭＳ ゴシック" , Font.BOLD , 12);
    	final FontMetrics metrics = getFontMetrics(font); 
    	final long cTime = currentTime.getTimeInMillis();
    	final long startInMillis = cTime - (Config.th()*4*60*60*1000);
    	final String sTime = currentTime.get(Calendar.HOUR_OF_DAY) + ":" +
    		String.format("%02d", currentTime.get(Calendar.MINUTE)) + ":" +
    		String.format("%02d", currentTime.get(Calendar.SECOND));
    	final int x1 = 120, y1 = 90, wd1 = 400, ht1 = 200, lo1 = 0, hi1 = 50;
        final int x2 = 120, y2 = 350, wd2 = 400, ht2 = 200, lo2 = 0, hi2 = 100;
        final int yStat = 50, yMes = 70;
        final Color COLOR_INDEX[] = { //index番号に対応した色の宣言（固定）
		//   0           1           2          3            4
		Color.gray, Color.blue, Color.red, Color.green, Color.orange,
		//   5              6           7           8             9
		Color.magenta, Color.pink, Color.cyan, Color.yellow, Color.darkGray
        };

        class Graphs {
        	public void analyze(DataSummary dsTemp[], DataSummary dsHumi[]) {
        		int dsIndex = 0;

        		for (SensorManager m : sMgr) {
        			if (m != null && m.type > 0) {
        				while(!sem.tryAcquire())
        					try{ Thread.sleep(20);} //arrayListが使えるまでセマフォを待つ
        					catch(InterruptedException ie) {}
        				for(StringBuffer b : arrayList) {
        					int second;
        					long time = Long.parseLong(b.substring(0, 13), 10);
        					String record = b.substring(14);
        					if (record.length() < 16) continue; // 短いレコードはエラーなので無視
        					long serial = Long.parseLong(record.substring(2, 14), 16);

        					//シリアルが該当して表示範囲の時間なら、時間とデータの配列を作る
        					if (m.serial != serial || time < startInMillis)
        						continue;
        					second = (int) ((time - startInMillis + 500) / 1000);

        					int lower, upper;
        					int remain, per_c;
        					double temperature = Double.NEGATIVE_INFINITY; //データ無しを示す初期値
        					double humidity = Double.NEGATIVE_INFINITY; //データ無しを示す初期値
        					int vad, vdd;
        					switch(m.type) { //センサ型別にレジスタを読み出し
        					case 0x10: // DS18S20
        						lower = Integer.parseInt(record.substring(16, 18), 16);
        						upper = Integer.parseInt(record.substring(18, 20), 16);
        						temperature = (upper << 8 | lower) / 2;
        						remain = Integer.parseInt(record.substring(28, 30), 16);
        						per_c = Integer.parseInt(record.substring(30, 32), 16);
        						temperature = temperature - 0.25 + (per_c - remain) / per_c;
        						break;
        					case 0x22: // DS1822
        						lower = Integer.parseInt(record.substring(16, 18), 16);
        						upper = Integer.parseInt(record.substring(18, 20), 16);
        						temperature = (upper << 8 | lower) / 16;
        						break;
        					case 0x26: // DS2438
        						lower = Integer.parseInt(record.substring(36, 38), 16);
        						upper = Integer.parseInt(record.substring(38, 40), 16);
        						temperature = ((upper << 8 | lower) >> 4) / 16;
        						lower = Integer.parseInt(record.substring(22, 24), 16);
        						upper = Integer.parseInt(record.substring(24, 26), 16);
        						vdd = upper << 8 | lower;
        						lower = Integer.parseInt(record.substring(40, 42), 16);
        						upper = Integer.parseInt(record.substring(42, 44), 16);
        						vad = upper << 8 | lower;
        						humidity = (double) vad / (double) vdd;
        						humidity = (humidity - 0.16) / 0.0062;
        						break;
        					default: // Others
        						break;
        					} //switch

        					// 同一シリアル番号で温度と湿度のデータが来る場合に振り分ける
        					if (temperature != Double.NEGATIVE_INFINITY) { //データ取得済
        						if (dsTemp[dsIndex] == null)
        							dsTemp[dsIndex] = new DataSummary(lo1, hi1, wd1, ht1, m);
        						dsTemp[dsIndex].add(second, temperature);
        					}
        					if (humidity != Double.NEGATIVE_INFINITY) { //データ取得済
        						if (dsHumi[dsIndex] == null)
        							dsHumi[dsIndex] = new DataSummary(lo2, hi2, wd2, ht2, m);
        						dsHumi[dsIndex].add(second, humidity);
        					}
        				} //for(StringBuffer b : arrayList)
        				sem.release(); //セマフォの開放
        				dsIndex++;
        			} //if (m != null && m.type > 0)
        		} //for (SensorManager m : sMgr)
        	} // public void analyze()

        	public void scale(String label, String unit, int x, int y, 
        			int wd, int ht, int low, int high) {

        		Calendar workTime = Calendar.getInstance();
        		String t;
        		long workInMillis;
        		long point;

        		g.setColor(Color.black); //グラフの外枠
        		g.setFont(font);
        		g.drawRect(x, y, wd, ht);
        		t = label; // "温度","湿度"
        		g.setColor(Color.black);
        		g.setFont(font);
        		g.drawString(t,	x/6,y + metrics.getHeight()/2);
        		for(int i = 0; i <= 5; i++) { //目盛りワクの描画
        			t = (high - i*(high-low)/5) + unit;
        			g.setColor(Color.black);
        			g.setFont(font);
        			g.drawString(t,	x - metrics.stringWidth(t),
        					y+ht/5*i + metrics.getHeight()/2);
        			if (i > 0 && i < 5) {
        				g.setColor(Color.LIGHT_GRAY);
        				g.drawLine(x, y+ht/5*i, x+wd, y+ht/5*i);
        			}
        		} //for
        		workInMillis = targetTime.getTimeInMillis();
        		for(int i = 0; i < 4; i++) { //時刻目盛りの描画
        			point = wd * (workInMillis - startInMillis) / (Config.th()*4*60*60*1000);
        			workTime.setTimeInMillis(workInMillis);
        			t = workTime.get(Calendar.HOUR_OF_DAY) + ":00";
        			g.setColor(Color.black);
        			g.drawString(t,	(int) (x + point) - metrics.stringWidth(t) / 2,
        					y + ht + metrics.getHeight() * 1);
        			t = workTime.get(Calendar.YEAR) + "/" +
        				(workTime.get(Calendar.MONTH) + 1) + "/" +
        				workTime.get(Calendar.DATE);
        			g.drawString(t,	(int) (x + point) - metrics.stringWidth(t) / 2,
        					y + ht + metrics.getHeight() * 2);
        			g.setColor(Color.LIGHT_GRAY);
        			g.drawLine((int) (x + point), y, (int) (x + point), y + ht);
        			workInMillis += (Config.th()*60*60*1000);
        		} //for
        		// 現在時刻の表示
        		g.setColor(Color.black);
        		g.clearRect(x + wd, y + ht,
        				metrics.stringWidth(sTime),metrics.getHeight());
        		g.drawString(sTime, x + wd,
        				y + ht + metrics.getHeight() * 1);
        	} //public void scale()
        	
        	public void legend(SensorManager[] sM, int x, int y, Font lFont) {
        		Font CurrentFont = g.getFont();
        		FontMetrics lmetrics = getFontMetrics(lFont); 

        		g.setFont(lFont);
        		for (SensorManager m : sM) //センサ情報から凡例を作る
        			if (m != null && m.type > 0) {
        				y += lmetrics.getHeight();
        				g.setColor(COLOR_INDEX[m.index]); //index番号=固定色
        				g.drawString(new String(m.name), x, y);
        			}
        		g.setFont(CurrentFont);
        	} //public void legend()

        	public void currentTime(int x, int y,
        				int wd, int ht, int low, int high) {
        		g.setColor(Color.black);
        		g.setFont(font);
        		g.clearRect(x + wd, y + ht,
        				metrics.stringWidth(sTime),metrics.getHeight());

        		g.drawString(sTime, x + wd,
        				y + ht + metrics.getHeight() * 1);
        		if (Config.level() > 0) { System.out.println("currentTime() " + sTime); }
        	} //public void currentTime()
        } //class Graph

        //** paint -- メイン処理 **//
	
        //データ解析と再描画の実行を判断する条件：毎Config単位秒に1回（6時間→6秒毎）
        if (graphControl || currentTime.get(Calendar.SECOND) % Config.th() == 0) {
        	if (Config.level() > 2) { System.out.println("drawingGraph: " + graphControl); }

        	//裏画面(g)クリア
        	g.clearRect(0, 0, d.width, d.height);

        	//目盛を枠を描画する（裏画面）
        	new Graphs().scale("温度", "℃", x1, y1, wd1, ht1, lo1, hi1);
        	new Graphs().scale("湿度", "％", x2, y2, wd2, ht2, lo2, hi2);
        	new Graphs().legend(sMgr, x1/2+wd1, y1/12, statusFont);

        	//データの解析
        	DataSummary dsTemp[] = new DataSummary[10];
        	DataSummary dsHumi[] = new DataSummary[10];
        	new Graphs().analyze(dsTemp, dsHumi);

        	//グラフの各ドットの代表値を求める
        	for(DataSummary s : dsTemp)
        		if (s != null && s.sIndex > 0) { s.summary(x1, y1); }
        	for(DataSummary s : dsHumi)
        		if (s != null && s.sIndex > 0) { s.summary(x2, y2); }

        	//グラフの描画
        	for(DataSummary s : dsTemp)
        		if (s != null && s.sIndex > 0) {
        			g.setColor(COLOR_INDEX[s.sm.index]);
        			g.drawPolyline(s.dx, s.dy, s.dotCount());
        		}
        	for(DataSummary s : dsHumi)
        		if (s != null && s.sIndex > 0) {
        			g.setColor(COLOR_INDEX[s.sm.index]);
        			g.drawPolyline(s.dx, s.dy, s.dotCount());
        		}
        	graphControl = false;
        } //if (graphControl || currentTime.get(Calendar.SECOND) % Config.th() > 0)
        else {
        	//グラフ描画をせずに、古いグラフに現在時刻の表示だけする
        	new Graphs().currentTime(x1, y1, wd1, ht1, lo1, hi1);
        	new Graphs().currentTime(x2, y2, wd2, ht2, lo2, hi2);
        	//message表示場所をクリア
        	g.clearRect(0, yStat - metrics.getHeight(),
        			x1/2+wd1, yMes - yStat + metrics.getHeight() + 5);
        }

        g.setFont(statusFont);
        if (status != null) { // Statusの表示
        	g.setColor(Color.black);
        	g.drawString(status + media,
        			(d.width-metrics.stringWidth(status + media))/2, yStat);
        }
        if (message != null) { // メッセージの表示
        	if (messageColor != null) { g.setColor(messageColor); };
        	g.drawString(message,
        			(d.width-metrics.stringWidth(message))/2, yMes);
        	g.setColor(Color.black);
        }
        g.setFont(CurrentFont);
        gs.drawImage(buffer, 0, 0, this);
    } //public void paint()
	
	public void update(Graphics gs) { paint(gs); }
	
	public void destroy() {
		if (Config.level() > 0) { System.out.println("destroy()"); }
		recorder.close();
	}

	public void actionPerformed(ActionEvent e) {
		Object ev = e.getSource();
		int count;

		if (ev == btStart && timer == null) { //停止中のStart
			status = "実行中：";
			message = "Startしました";
			messageColor = Color.black;
			btStart.setEnabled(false); //各ボタンの表示
			btStop.setEnabled(true);
			btClear.setEnabled(false);
			btLoad.setEnabled(false);
			btSave.setEnabled(!recorder.isFile());
			btFile.setEnabled(false);

			if (Config.level() > 0) { System.out.println("START"); }

			if (timer != null) { //デバッグ用：通常はあり得ない
			  	System.out.println("*** Invalid timer status(!= null), isAlive = "
			  			+ timer.isAlive());
			}
			else {
			    if (Config.level() > 1) { System.out.println("**Start TIMER"); };
			    timer = new Thread(this);
			    timer.setName("Timer"); //Timerスレッド起動
			    timer.start();
			    recorder.open();
			    if (receiver != null)
			    	System.out.println("*** Invalid receiver status!");
			    receiver = new Thread(this);
			    receiver.setName("Receiver"); //Receiverスレッド起動
			    receiver.start();
			}
		}
		else if (ev == btStop) { //動作中のStop
			if (Config.level() > 1) { System.out.println("**Null TIMER"); }
			timer = null;
			status = "停止：";
			message = "Stopしました";
			messageColor = Color.black;
			btStart.setEnabled(true); //各ボタンの表示
			btStop.setEnabled(false);
			btClear.setEnabled(true);
			btLoad.setEnabled(true);
			btSave.setEnabled(true);
			btFile.setEnabled(true);

			recorder.close(); 
			if (Config.level() > 1) { System.out.println("**Waitfor Receiver JOIN..."); }
			try { receiver.join(); }
			catch(InterruptedException ie){
				    System.out.println("Exception-Stop : " + ie);
			}
			receiver = null;
			graphControl = true;
			if (Config.level() > 0)
				System.out.println("**Stop: count = " + arrayList.size());
		}
		else if (ev == btClear) { //データClear処理
			count = arrayList.size();
			message = "データClear：" + count + "件のデータ";
			messageColor = Color.red;
			graphControl = true;
			if (count > 0) {
				dConfirm.setVisible(true);
				System.out.println("**Clear: " + count);
			}
		}
		else if (ev == btLoad) { //データLoad処理
			if ((count = arrayList.size()) > 0) {
				message = "データClear：" + count + "件のデータ";
				messageColor = Color.red;
				dConfirm.setVisible(true);
				if (Config.level() > 0)
					System.out.println("**Clear beforeLoad: " + count);
			}
			if (Config.level() > 0) { System.out.println("*** Load " + count); }
			if ((count = arrayList.size()) == 0) {
				fdLoad.setVisible(true);
				if (fdLoad.getFile() != null) { //Loadファイル名の取得
					String dir = fdLoad.getDirectory();
					String file = fdLoad.getFile();
					String line;
					try { //Loadファイルからのデータ読み出し
						BufferedReader br = new BufferedReader(
								new FileReader(new File(dir, file)));
						while((line = br.readLine()) != null) {
							if (Config.level() > 2)
								System.out.println("<" + line + ">");
							arrayList.add(new StringBuffer(line));
						}
						br.close();
					}
					catch(IOException ie){
						System.out.println("Exception-Load : " + ie);
					}
					message = "Load済ファイル：" + dir + file
						+ "（" + arrayList.size() + "件）";
					messageColor = Color.blue;
					graphControl = true;
				}
			}
		}
		else if (ev == btSave) { //データSave処理
			fdSave.setVisible(true);
			if (fdSave.getFile() != null) {
				recorder = new Recorder(fdSave.getDirectory(), fdSave.getFile());
			}
			if (recorder.fileName() != null) {
				message = "Save済ファイル：" + recorder.fileName();
				messageColor = Color.red;
				recorder.store(arrayList);
			}
		}
		else if (ev == btFile) { //File記録の切り替え（トグル）
			message = "データ記録先を変更しました";
			messageColor = Color.green;
			if (recorder.setFile()) {
				if (recorder.fileName() == null) {
					fdSave.setVisible(true);
					if (fdSave.getFile() != null) {
						recorder = new Recorder(fdSave.getDirectory(), fdSave.getFile());
					}
					else {
						System.out.println("File: getFile() == null");
						recorder.setFile(false);
					}
				}
			}
			if (Config.level() > 0)
				System.out.println("File: recordFile: " + recorder.fileName());
			if (recorder.isFile()) {
				btSave.setEnabled(false);
				media = "データ記録先：" + recorder.fileName();
				recorder.store(arrayList);
			}
			else {
				btSave.setEnabled(true);
				media = "データ記録先：（メモリ）";
			}
		}
		else if (ev == btOK) { //データClearのOK確認
			if (Config.level() > 1) { System.out.println("**btOK "); }
			message = "OK! Clearしました";
			messageColor = Color.red;
			arrayList.clear();
			dConfirm.setVisible(false);
		}
		else if (ev == btCancel) { //データClearのCancel
			if (Config.level() > 1) { System.out.println("**btCancel "); }
			message = "Cancelしました";
			messageColor = Color.green;
			dConfirm.setVisible(false);
		}
		repaint();
	}
}
