/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.plugin.view;

import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.ColorMap;				// 6.0.2.1 (2014/09/26)
import org.opengion.fukurou.system.OgBuilder ;			// 6.4.4.1 (2016/03/18)

import java.util.Calendar;
import java.util.Map;
import java.util.TreeMap;
import java.util.Collection;
import java.util.Locale ;
// import java.util.Date;								// 7.0.5.1 (2019/09/27) Date → Calendar 変更
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Stroke;
import java.awt.BasicStroke;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;

/**
 * キー、日時、状況コードを持つ稼働状況の表示を行うクラスです。
 *
 * パラメータが必要な場合は、ViewTimeBarParamTag を使用してください。
 *
 * パラメータが設定されていない場合は、ViewForm_ImageTimeBar の初期値が使用されます。
 * (パラメータを使用するには、viewタグのuseParam 属性をtrueに設定する必要があります。)
 *
 * SELECT文は、キー、日時、状況コードが、必須項目で、カラムの並び順は、完全に固定です。
 * よって、カラム位置を指定する必要はありませんが、SELECT文を自由に設定することも
 * 出来ませんので、ご注意ください。
 * この固定化に伴い、WRITABLE 指定も使用できません。（そもそも書き込み不可です）
 * それ以降のカラムについては、内部処理としては、使用していません。
 * ただし、パラメータで、カラー色指定、ラベル表記部、イメージ重ね合わせ、
 * ポップアップ表記、リンク表記に使えます。
 *
 * データの並び順(ORDER BY)も、キー、日時順にしてください。
 * データは、キー単位に１レコード作成されます。（キーブレイク）その間、日時順に
 * データを処理します。
 *
 * データの表示は、今のレコードの日時から、次のレコードの日時までを一つの状態と
 * して表します。今のレコードを表示するには、次のレコードが必要になります。
 * 画面表示は、表示開始日時(minStartTime) から 表示期間(timeSpan)分を表示します。
 * 通常、開始時刻は、表示開始時刻より前より始まり、次のレコードで、終了時刻が決定
 * されます。最後のデータは、期間満了まで続いていると仮定されます。
 * データが存在しないのであれば、「存在しないデータ」を作成してください。
 * 
 * ImageTimeBar では、キーでまとめた値について、各状況コードをカラー化し、積み上げ
 * 帯グラフ形式でPNG画像化します。
 * この画像を、読み込む HTML を出力することで、画面上に、積み上げ帯グラフを表示します。
 * 状況コードに対応する色は、標準では自動作成ですが、外部から色文字列(colorClm)を与えることで
 * 自由に指定する事も可能です。
 *
 * ポップアップ表記(tipsClm)、リンク表記(linkClm)は、この画像に対するエリア指定タグを出力する事で実現します。
 * 画像ファイルは、全データに対して、１画像だけなので、サイズは大きくなりますが、１レコード
 * 単位に画像を作成しないため、レスポンスは向上します。
 * それぞれ、viewMarker , viewLink を利用することが可能です。特に、リンク表記(linkClm) については、
 * linkタグの hrefTarget 属性を true に設定することで適用できます。
 *
 * 画像ファイルは、java.io.File.createTempFile( File ) で作成するため、JavaVM(=Tomcat)が
 * 正常終了するときに、削除されます。異常終了時には残りますが、temp フォルダを定期的に
 * 整理すれば、それほど大量のファイルが残ることはないと思われます。
 *
 * データは、イベント発生時に作成されると仮定しています。つまり、書き込まれた日時から、
 * 状況コードに対応する状況が発生し、次の状況違いのレコードまで継続していると考えます。
 * よって、データを途中で切り出す場合、切り出す範囲の前の状態が必要になります。
 * 一番最初の状態は、"不明" として扱います。（空欄＝白色）
 *
 * @og.rev 5.5.5.6 (2012/08/31) 新規追加
 * @og.group 画面表示
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ViewForm_ImageTimeBar extends ViewForm_HTMLTable {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "7.1.0.0 (2020/01/20)" ;

	private static final Color LABEL_COLOR	= Color.BLACK;		// ラベル記述時の色
	private static final Color NULL_COLOR	= Color.WHITE;		// 5.6.1.1 (2013/02/08) 不明(空欄)時の色

	private static final int	M_60		= 60;				// 6.1.0.0 (2014/12/26) refactoring
	private static final int	H_24		= 24;				// 6.1.0.0 (2014/12/26) refactoring
//	private static final int	W_7			= 7;				// 6.1.0.0 (2014/12/26) refactoring  7.0.5.1 (2019/09/27) 削除
	private static final int	DEC			= 10;				// 6.1.0.0 (2014/12/26) refactoring

	private static final int	MINUTE_OF_DAY = 24 * 60 ;		// 6.0.2.0 (2014/09/19) １日が何分 なのか(=1440)
	private static final long	MILLI_MINUTE  = 60 * 1000L;		// 6.0.2.0 (2014/09/19) ミリ秒に換算した分。(=60000L)

//	// 5.6.5.0 (2013/06/07) 曜日データを配列で持っておきます。
//	// 6.4.1.1 (2016/01/16) DAY_OF_WEEK_ja → DAY_OF_WEEK_JA refactoring
//	// 7.0.5.1 (2019/09/27) 削除
//	private static final String[] DAY_OF_WEEK_JA = { "土","日","月","火","水","木","金","土" };	// [0]="土" は、1～7 の余計算で、 7=0 になる為。

	private long startDate		;	// タイムテーブルの表示開始日時から求めた long 値(1=分単位)
	private long timeSpan		;	// タイムテーブルの表示期間。元は時間指定であるが、分単位で持つ。(1=分単位)

	private boolean useLegend	;	// カラーの凡例を使用するかどうか[true/false]
	private int maxLabelWidth	;	// ラベル表記部の最大サイズをpxで指定。何もなければ、可変長サイズ
	private int maxTimeWidth	;	// タイム表記部の最大サイズをpxで指定。
	private int chartHeight		;	// １レコードのチャートの間隔をpxで指定。実際の幅は、CHART_HEIGHT＋MARGIN＊２
	private int headerHeight	;	// 6.4.6.1 (2016/06/03) チャートのヘッダー部の高さ(初期値は、チャートの間隔:chartHeight)
	private int chartPadding	;	// イメージ作成の 全体テーブルの隙間
	private int recodeMargin	;	// 各レコード、文字等の内部の間隔
	private boolean useLastData	;	// 5.6.1.1 (2013/02/08) 行の最後の情報が、継続しているとして使うかどうか[true/false]
	private boolean isDebug		;	// 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします[true/false]。

	private String tempDir		;	// 画像ファイルを作成するテンポラリディレクトリ(相対パス)
	private String tempUrl		;	// 作成した画像ファイルを取得するときに使用するURL(コンテキスト/相対パス)

	private String headerUpFmt	;	// 7.1.0.0 (2020/01/20) ヘッダー部分上段の日付フォーマット
	private String headerDwFmt	;	// 7.1.0.0 (2020/01/20) ヘッダー部分下段の日付フォーマット

	// SELECT文は、キー、日時、状況コードが、必須項目
	// 6.4.1.1 (2016/01/16) keyClmNo → KEY_CLMNO , dyClmNo → DY_CLMNO , fgjClmNo → FGJ_CLMNO refactoring
	private static final int	KEY_CLMNO	= 0;	// ヘッダー1：キーカラムNo
	private static final int	DY_CLMNO	= 1;	// ヘッダー2：日時カラムNo
	private static final int	FGJ_CLMNO	= 2;	// ヘッダー3：状況コードカラムNo

	private int[] labelClmsNo	;					// 初期値は、キーカラム
	private int[] maxClmWidth	;					// labelClms 単位の最大文字列長
	private int   colClmNo		= -1;				// カラーコード直接指定する場合に色文字列を指定するカラムNo
	private int   tipsClmNo		= -1;				// マウスオーバー時のTips表示を行うカラムNo
	private int   linkClmNo		= -1;				// クリッカブルリンクを設定するカラムNo

	private int str2DateTime	;	// getStr2Date(String)処理をおこなった時の、引数の時刻情報(分単位)をセットするテンポラリ変数。
	private int startTime		;	// startDate の時刻情報。上記処理で、startDate を getStr2Date(String) 処理したときの値を持っておくための変数。
	private long nowTime		;	// 6.0.2.0 (2014/09/19) データがない最後の処理で使用する現在時刻

	// 6.3.9.0 (2015/11/06) Variables should start with a lowercase character(PMD)
	private int maxX	;	// イメージの最大横幅(X)方向のサイズpx。chartPadding*2 + maxLabelWidth + maxTimeWidth ;
	private int maxY	;	// イメージの最大縦幅(Y)方向のサイズpx。chartPadding*2 + (headerHeight+recodeMargin*2)*2 + (chartHeight+recodeMargin*2)*(rowCnt-2); 

	// 6.9.9.1 (2018/08/27) 必要なのかどうか不明。｢ラベル領域の長さ求めで、改行が含まれる場合の計算対応｣ が必要なのかを判断するﾌﾗｸﾞ。処理速度向上用
	private boolean useCR	;

	// imageHeaderPaintメソッドで使用する 破線の定義
	private static final Stroke DSAH_STROK = new BasicStroke(
				1.0f						, // BasicStroke の幅
				BasicStroke.CAP_BUTT		, // 両端の装飾
				BasicStroke.JOIN_MITER		, // 輪郭線セグメントの接合部の装飾
				1.0f						, // 接合トリミングの制限値
				new float[] {2.0f, 1.0f}	, // 破線パターンを表す配列
				0.0f						  // 破線パターン開始位置のオフセット
	);

	/**
	 * DBTableModel から HTML文字列を作成して返します。
	 * startNo(表示開始位置)から、pageSize(表示件数)までのView文字列を作成します。
	 * 表示残りデータが pageSize 以下の場合は,残りのデータをすべて出力します。
	 *
	 * @og.rev 5.6.1.0 (2013/02/01) tips や link は、ひとつ前のデータで作成する必要がる為、最初の1件目は、処理しないように修正
	 * @og.rev 5.6.1.1 (2013/02/08) 初期値の色を、直接の値ではなく、static final で定義された色を使用する。色自体は変えていません
	 * @og.rev 5.6.1.1 (2013/02/08) useLastData 追加
	 * @og.rev 5.6.3.0 (2013/04/01) tipsのシングルクォーテーション のエスケープ
	 * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、&lt;span&gt;タグが付与される値から、spanタグを削除します。
	 * @og.rev 6.0.2.0 (2014/09/19) データが存在しない最後の処理で、現在時刻まで、継続しているとします。
	 * @og.rev 6.4.2.0 (2016/01/29) alt属性にtitle属性を追記。
	 * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
	 * @og.rev 6.4.4.2 (2016/04/01) 引数を、Supplierクラスに変更して、結果を複数指定できるようにします。
	 * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
	 * @og.rev 6.4.7.1 (2016/06/17) useLastData属性は、最後の処理のみに適用する。
	 * @og.rev 6.4.8.2 (2016/07/08) 未来時刻の場合に、現在時刻まで巻き戻って塗りつぶしているので、未来時刻の判定を追加。
	 * @og.rev 6.7.5.0 (2017/03/10) COLOR_CLM の値を、getValueLabel で取得するように変更。
	 * @og.rev 6.9.7.0 (2018/05/14) imgタグに付与する、高さ、幅を、AbstractViewForm から、取得します。
	 * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
	 * @og.rev 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"／＞" 止めを、"＞" に変更します)。
	 * @og.rev 7.0.5.1 (2019/09/27) getTime2Str 修正に伴う変更。
	 *
	 * @param  startNo	  表示開始位置
	 * @param  pageSize   表示件数
	 *
	 * @return	DBTableModelから作成された HTML文字列
	 * @og.rtnNotNull
	 */
	@Override
	public String create( final int startNo, final int pageSize )  {
		if( getRowCount() == 0 ) { return ""; }	// 暫定処置

		// パラメータの情報を初期化＆取得します。
		paramInit();

		final int lastNo = getLastNo( startNo, pageSize );

		// イメージの 最大Ｘ、Ｙサイズを求め、結果を、maxX,maxY 変数に設定する。
		calcImageSize( startNo,lastNo );

		final BufferedImage img = new BufferedImage( maxX, maxY, BufferedImage.TYPE_INT_ARGB);
		final Graphics2D g2 = img.createGraphics();

		// chartPadding を考慮した領域のクリップ（クリップ外にDrowされても無視されます。）
		g2.setClip( chartPadding , chartPadding , maxX-chartPadding*2+1 , maxY-chartPadding*2+1 );	// （x 座標,y 座標,幅,高さ) ＋１は罫線の分

		final StringBuilder out = new StringBuilder( BUFFER_LARGE );

		String oldKeyVal = "";				// 初期値。１回目にキーブレイクさせる。
		long   oldTime   = 0L;				// 日付項目の一つ前の値

		Color  oldColor  = NULL_COLOR;		// 5.6.1.1 (2013/02/08) 不明(空欄)時の色(初期値)

		// 6.0.2.1 (2014/09/26) fukurou.util.ColorMap とクラス名がかぶるので、こちらを変更します。
		final FlgColorMap colMap = new FlgColorMap();	// 状況コード、ラベル、カラーを管理するクラス

		// 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
		// ヘッダー文の考慮。rowCnt は、使わず、imgY は、加算していきます。

		int imgY = chartPadding + (headerHeight+recodeMargin*2)*(useLegend ? 2 : 1);		// 6.4.6.1 (2016/06/03)

		final int rowH = chartHeight+recodeMargin*2;	// 罫線出力時の高さ(drawRectで使用)

		final double timeScale = (double)maxTimeWidth/(double)timeSpan;
		final boolean useTipsLink = tipsClmNo >= 0 || linkClmNo >= 0 ;		// tipsClm か linkClm かどちらかを使用する場合、true

		for( int row=startNo; row<lastNo; row++ ) {
			// キーブレイクの判定
			final String keyVal  = getValue( row,KEY_CLMNO );
			final String dyVal   = getValue( row,DY_CLMNO );

			// キーブレイク判定。キーブレイクは、一番初めから来る。
			if( !oldKeyVal.equals( keyVal ) ) {
				// キーブレイクで最初に行うのは、前のレコードの未出力分の処理。１行目は、処理不要
				if( row > startNo ) {
					// 6.0.2.0 (2014/09/19) データが存在しない最後の処理で、現在時刻まで、継続しているとします。
					long lastTime = nowTime ;		// 現在時刻(分単位に変換する)
					// 5.9.10.5 (2016/07/08) 現在時刻がtimeSpanより大きいor最終値より小さい場合は、lastTimeをtimeSpanにする。
					if( lastTime > timeSpan || lastTime < oldTime ) { lastTime = timeSpan; }	// 5.9.10.5 (2016/07/08)

					if( !useLastData ) { oldColor  = NULL_COLOR; }			// 6.4.7.1 (2016/06/17)
					while( oldTime < timeSpan ) {
						// 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
						final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);		// イメージ出力時の X軸の左端 px数
						final int imgW = (int)((lastTime-oldTime)*timeScale) ;							// イメージ出力時の 幅(fillRectで使用)

						if( isDebug ) {
//							final int step = TimeScaleStep.getStep( timeScale );
//							final String stTM = getTime2Str(startTime+(int)oldTime ,step,0,0) ;			// 7.0.5.1 (2019/09/27) 
//							final String edTM = getTime2Str(startTime+(int)lastTime,step,0,0) ;			// 7.0.5.1 (2019/09/27) 
							final String stTM = getTime2Str(startTime+(int)oldTime ) ;					// 7.0.5.1 (2019/09/27) 
							final String edTM = getTime2Str(startTime+(int)lastTime) ;					// 7.0.5.1 (2019/09/27) 
							System.out.println( "② ROW[" + row + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
						}
						imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, row, out);	// 6.0.2.0 (2014/09/19)

						oldTime   = lastTime ;
						lastTime  = timeSpan ;
						oldColor  = NULL_COLOR;		// 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
					}
					imgY += rowH ;					// 初期値にヘッダー高さを加算しているため、２回目以降に加算していく。 6.4.6.1 (2016/06/03)
				}

				// 次に、新しい行の出力（ラベルは、ブレイク時に出力しておきます。）
				oldKeyVal = keyVal;			// null は来ないはず
				// 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
				// ヘッダー文の考慮。rowCnt は、使わず、imgY は、加算していきます。
				// レコードのラベル分だけ、繰返し描画する。
				final int clmSu = labelClmsNo.length;
				int posX  = chartPadding ;								// ラベル文字列の書き出し位置の初期値
				final int posY2 = imgY+chartHeight+recodeMargin ;		// ラベルのY軸基準。
				g2.setColor( LABEL_COLOR );
				for( int col=0; col<clmSu; col++ ) {
					final String lbl = StringUtil.spanCut( getValueLabel( row,labelClmsNo[col] ) );		// 5.6.3.1 (2013/04/05) spanタグを削除
					// 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
//					g2.drawString( lbl , posX + recodeMargin, posY2 );									// 文字列,ベースラインのx座標,y座標
					if( useCR ) {
						final String[] xlbls = lbl.split( "\\n|\\\\n" );									// 本物の改行と、"\n" 文字列
						for( int i=0; i<xlbls.length; i++ ) {
							final int posY3 = posY2 - (xlbls.length-1-i)*rowH/xlbls.length;					// 改行ラベルを、均等に枠内に分割します。
							g2.drawString( xlbls[i] , posX + recodeMargin, posY3  );						// 文字列,ベースラインのx座標,y座標
						}
					}
					else {
						g2.drawString( lbl , posX + recodeMargin, posY2 );									// 文字列,ベースラインのx座標,y座標
					}

					posX += recodeMargin*2 + maxClmWidth[col] ;				// 次の書き出し位置は、文字最大長＋マージン
				}

				// レコードの枠線
				g2.drawRect( chartPadding , imgY , maxX-chartPadding*2 , rowH );		// (レコード枠)左端x,上端y,幅w,高さh

				oldTime = 0L;				// キーブレイクによる初期化
				oldColor= NULL_COLOR;		// 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
			}

			long newTime = getStr2Date( dyVal ) - startDate;		// 次の時刻
			if( newTime < oldTime ) { newTime = oldTime; }			// 前の時刻より小さい場合は、前の時刻まで、進めておく。

			// 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
			final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);		// old時刻から書き始める(左端x)
			final int imgW = (int)((newTime-oldTime)*timeScale) ;							// 差分幅だけ描画する。

			if( isDebug ) {
//				final int step = TimeScaleStep.getStep( timeScale );
//				final String stTM = getTime2Str(startTime+(int)oldTime,step,0,0) ;			// 7.0.5.1 (2019/09/27) 
//				final String edTM = getTime2Str(startTime+(int)newTime,step,0,0) ;			// 7.0.5.1 (2019/09/27) 
				final String stTM = getTime2Str(startTime+(int)oldTime) ;					// 7.0.5.1 (2019/09/27) 
				final String edTM = getTime2Str(startTime+(int)newTime) ;					// 7.0.5.1 (2019/09/27) 
				System.out.println( "① ROW[" + row + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
			}
			imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, row, out);	// 6.0.2.0 (2014/09/19)

			oldTime = newTime ;

			final String fgjVal  = getValue( row,FGJ_CLMNO );			// 状況コードのキー
			final String fgjLbl  = getValueLabel( row,FGJ_CLMNO );		// 状況コードのラベル
			if( colClmNo >= 0 ) {
				// 6.7.5.0 (2017/03/10) COLOR_CLM の値を、getValueLabel で取得するように変更。
				oldColor  = colMap.getColor( fgjVal,fgjLbl,getValueLabel( row,colClmNo ) );	// 色文字列を指定する場合
			}
			else {
				oldColor  = colMap.getColor( fgjVal,fgjLbl );								// 色文字列を指定しない＝自動設定
			}
		}

		// レコードのいちばん最後の出力。一応、ジャストの場合(oldTime == maxEdTime)は、処理しない
		// 5.6.1.1 (2013/02/08) レコードのいちばん最後の出力は、NULL色を使うように変更
		long lastTime = nowTime ;		// 現在時刻(分単位に変換する)
		// 6.4.8.2 (2016/07/08) 未来日付の場合に、現在時刻まで巻き戻って塗りつぶしている。
		if( lastTime > timeSpan || lastTime < oldTime ) { lastTime = timeSpan; }		// 現在時刻が、timeSpanより大きい場合 OR 未来時刻の場合は、同じにする。

		if( !useLastData ) { oldColor  = NULL_COLOR; }			// 6.4.7.1 (2016/06/17)
		while( oldTime < timeSpan ) {
			// 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
			final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);		// イメージ出力時の X軸の左端 px数
			final int imgW = (int)((lastTime-oldTime)*timeScale) ;							// イメージ出力時の 幅(fillRectで使用)

			if( isDebug ) {
//				final int step = TimeScaleStep.getStep( timeScale );
//				final String stTM = getTime2Str(startTime+(int)oldTime ,step,0) ;			// 7.0.5.1 (2019/09/27) 
//				final String edTM = getTime2Str(startTime+(int)lastTime,step,0) ;			// 7.0.5.1 (2019/09/27) 
				final String stTM = getTime2Str(startTime+(int)oldTime ) ;					// 7.0.5.1 (2019/09/27) 
				final String edTM = getTime2Str(startTime+(int)lastTime) ;					// 7.0.5.1 (2019/09/27) 
				System.out.println( "③ ROW[" + lastNo + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
			}
			imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, lastNo, out);	// 6.0.2.0 (2014/09/19)

			oldTime   = lastTime ;
			lastTime  = timeSpan ;
			oldColor  = NULL_COLOR;		// 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
		}

		// ヘッダー情報のイメージを作成します。
		imageHeaderPaint( g2 , timeScale , colMap );

		// イメージファイルを出力し、URLを返します。
		final File file ;				// 6.3.9.0 (2015/11/06) Found 'DU'-anomaly for variable(PMD)
		try {
			final File dir = new File( tempDir );		// 画像ファイル出力先のフォルダ
			// 6.0.2.4 (2014/10/17) RV  java.io.File.mkdirs() の例外的戻り値を無視しています。 
			if( ! dir.exists() && ! dir.mkdirs() ) {
				final String errMsg = "画像ファイル出力先のフォルダの作成に失敗しました。tempDir=[" + dir +"]" ;
				throw new HybsSystemException( errMsg );
			}

			file = File.createTempFile( "Img",".png", dir );	// テンポラリファイルの作成
			file.deleteOnExit();								// JavaVM 停止時にテンポラリファイルの削除

			ImageIO.write(img, "PNG", file );
			g2.dispose();
		}
		catch( final IOException ex ) {
			final String errMsg = "画像ファイルの作成に失敗しました。tempDir=[" + tempDir +"]" ;
			throw new HybsSystemException( errMsg,ex );
		}

		// 6.9.7.0 (2018/05/14) imgタグに付与する、高さ、幅を、AbstractViewForm から、取得します。
		// 高さ、幅は、一つでも指定されている場合は、AbstractViewForm の設定値を使用し、どちらも未設定の場合は、
		// 元の画像サイズ(maxX,maxY)を記述します。(imgタグのサイズ指定で、ブラウザの画像読み込み時の体感速度が向上します。)
		String width  = getWidth();			// viewタグで設定した、幅
		String height = getHeight();		// viewタグで設定した、高さ

		if( StringUtil.isEmpty( width ) && StringUtil.isEmpty( height ) ) {		// 両方とも、empty の場合のみ、ｵﾘｼﾞﾅﾙの値をセットします。
			width  = maxX + "px" ;
			height = maxY + "px" ;
		}

		// imgタグを作成します。
		// 6.3.9.0 (2015/11/06) width,height にセットしているが、値を変更していないので、直接設定する。
		// 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
		return new OgBuilder()
			.append( "<img id='ImageTimeBar' alt='ImageTimeBar' title='ImageTimeBar' border='0'" )
			.appendNN( " width='"	, width						, "'" )			// width が null の場合は、この行すべて登録しません。
			.appendNN( " height='"	, height					, "'" )			// heightが null の場合は、この行すべて登録しません。
			.append(   " src='"		, tempUrl , file.getName()	, "'" )
//		return new OgBuilder()
//			.append( "<img id='ImageTimeBar' alt='ImageTimeBar' title='ImageTimeBar' border='0'"
//					, " width='"		, String.valueOf( maxX )
//					, "px' height='"	, String.valueOf( maxY )
//					, "px' src='"		, tempUrl
//					, file.getName() , "'" )
			// 6.4.4.2 (2016/04/01) 引数がSupplierに変更。
			// area タグのデータが作成されていれば、出力します。
			// 7.0.1.0 (2018/10/15)
			.appendCase( out.length() > 0
//							, () -> new String[] {	" usemap='#TimeBarMap' /><map name='TimeBarMap'>"
							, () -> new String[] {	" usemap='#TimeBarMap' ><map name='TimeBarMap'>"
												  , out.toString()
												  , "</map>" }				// true
//							, () -> new String[] {	" />" }					// false
							, () -> new String[] {	" >" }					// false
			).toString();
	}

	/**
	 * データが存在しない最後の処理で、現在時刻まで、継続している処理をまとめます。
	 *
	 * このメソッドは、キーブレイク、通常の出力処理、データの最後の３か所に同じ処理が出てくるため
	 * 一つに整理しました。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規作成
	 * @og.rev 6.4.2.0 (2016/01/29) alt属性にtitle属性を追記。
	 * @og.rev 6.4.7.1 (2016/06/17) useLastData属性は、最後の処理のみに適用する。
	 * @og.rev 6.4.8.2 (2016/07/08) クリッカブルマップの設定で、普通のリンクタグや、hrefOnly、hrefTarget が使えるようにする。
	 * @og.rev 6.4.9.5 (2016/09/09) title属性を廃止したが、一旦戻します。(ネイティブモードでツールチップが表示しない)
	 * @og.rev 6.7.9.1 (2017/05/19) 幅が0の場合は、1ピクセルだけ一つ前の領域に描画する。
	 * @og.rev 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"／＞" 止めを、"＞" に変更します)。
	 * @og.rev 7.0.2.1 (2019/03/04) linkClmNoのhrefVal で、空文字列もﾘﾝｸの使用可否の判定条件に入れる
	 *
	 * @param  g2	  		描画オブジェクト(Graphics2D)
	 * @param  oldColor		旧色
	 * @param  imgX	 		イメージ出力時の X軸の左端 px数
	 * @param  imgY  		イメージ出力時の Y軸の上端 px数
	 * @param  imgW	 		イメージ出力時の 幅(fillRectで使用)
	 * @param  useTipsLink	Tipsのリンクを作成するかどうか(true:作成する)
	 * @param  row			行番号
	 * @param  outBuf		出力用 StringBuilder
	 */
	private void imageMeker( final Graphics2D g2,final Color oldColor,
							 final int imgX,final int imgY,final int imgW,
							 final boolean useTipsLink,final int row,final StringBuilder outBuf ) {
		// 幅が0以上の場合のみ描画する。
		if( imgW > 0 ) {
			// 6.4.7.1 (2016/06/17)
			g2.setColor( oldColor );
			g2.fillRect( imgX , imgY+recodeMargin , imgW, chartHeight );	// (実際の状態)左端x,上端y,幅w,高さh

			// 6.0.2.0 (2014/09/19) Tips表示を出さない条件に、oldColor == NULL_COLOR を加える。
			// equals ではなく、!= でオブジェクトの直接比較で判定する。
			// 6.4.7.1 (2016/06/17)
			final boolean useTips = ( oldColor != NULL_COLOR ) && useTipsLink && ( row > 0 ) ;

			// tipsClm か linkClm を使用する。
			// 5.6.1.0 (2013/02/01) tips や link は、ひとつ前のデータで作成する必要がる為、最初の1件目は、処理しないように修正
			if( useTips ) {
				// tips や link は、ひとつ前のデータで作成する必要がある。(row-1)
				String tips = tipsClmNo >= 0 ? getValueLabel( row-1,tipsClmNo ) : getValueLabel( row-1,FGJ_CLMNO ) ;
				if( tips != null && tips.indexOf( '\'' ) >= 0 ) {		// 5.6.3.0 (2013/04/01) シングルクォーテーション のエスケープ
					tips = tips.replaceAll( "'","&#39;" );
				}
				tips = StringUtil.spanCut( tips );						// 5.6.3.1 (2013/04/05) spanタグを削除

				outBuf.append( "<area shape='rect' alt='" ).append( tips ).append( "' title='" ).append( tips ).append( '\'' );			// 6.4.9.5 (2016/09/09) 一旦戻す。
				if( linkClmNo >= 0 ) {
					final String hrefVal = getValueLabel( row-1,linkClmNo );
					// 7.0.2.1 (2019/03/04) 空文字列も判定条件に入れる
//					if( hrefVal != null ) {
					if( hrefVal != null && hrefVal.trim().length() > 0 ) {
						// 6.4.8.2 (2016/07/08) クリッカブルマップの設定で、普通のリンクタグや、hrefOnly、hrefTarget が使えるようにする。
						outBuf.append( ' ' );
						final int stAd = hrefVal.indexOf( "href" );
						final int edAd = hrefVal.indexOf( '>' );
						if( stAd >= 0 ) {				// href が存在する。
							if( edAd >= 0 ) { outBuf.append( hrefVal.substring( stAd,edAd ) ); }	// a href ･･･ の場合の処置
							else            { outBuf.append( hrefVal.substring( stAd ) ); }			// 従来の互換設定
						}
						else {
							outBuf.append( " href='" ).append( hrefVal ).append( '\'' );			// リンクタグの hrefOnlyや、hrefTarget が使えるようにする。
						}
					}
				}

				// 6.4.8.2 (2016/07/08) title を、tips からキーの値に変更。ただし、未設定の場合のみ。
				if( outBuf.indexOf( "title=" ) < 0 ) {
					final String title = getValueLabel( row-1,KEY_CLMNO );	// 6.4.8.2 (2016/07/08) title を、tips からキーの値に変更。
					outBuf.append( "' title='" ).append( title ).append( '\'' );
				}

				// 6.0.2.5 (2014/10/31) char を append する。
				outBuf.append( " coords='" ).append( imgX )
					.append( ',' ).append( imgY+recodeMargin )				// 左端x1,上端y1
					.append( ',' ).append( imgX+imgW )
					.append( ',' ).append( imgY+recodeMargin+chartHeight )	// 右端x2,下段y2
//					.append( "' />\n" );
					.append( "' >\n" );										// 7.0.1.0 (2018/10/15)
			}
		}
		//  6.7.9.1 (2017/05/19) 幅が0の場合は、1ピクセルだけ一つ前の領域に描画する。
		else {
			g2.setColor( oldColor );
			g2.fillRect( imgX -1 , imgY+recodeMargin , 1, chartHeight );	// (実際の状態)左端x,上端y,幅w,高さh
		}
	}

	/**
	 * パラメータ内容を初期化します。
	 *
	 * @og.rev 5.6.1.1 (2013/02/08) useLastData 追加
	 * @og.rev 6.0.2.0 (2014/09/19) nowTime データがない最後の処理で使用する現在時刻
	 * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
	 * @og.rev 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします。
	 * @og.rev 7.1.0.0 (2020/01/20) ヘッダー部分の日付フォーマット指定の追加
	 */
	private void paramInit() {
		final String s_startDate	= getParam( "START_DATE"		);	// タイムテーブルの表示開始日時をセットします(初期値:データの最小日時)。
		final int timeSpanHour		= getIntParam( "TIME_SPAN"		);	// タイムテーブルの表示期間を時間で指定します(初期値:{@og.value TIME_SPAN})。

		final String[] s_labelClms	= StringUtil.csv2Array( getParam( "LABEL_CLMS" ) );	// 一覧表のラベル表示部に表示するカラム名をCSV形式で指定します。
		final String s_colClm		= getParam( "COLOR_CLM"		);	// レコードに付ける色を色文字列で指定する場合のカラム名を指定します。
		final String s_tipsClm		= getParam( "TIPS_CLM"		);	// レコード単位に、マウスオーバー時のTips表示を行うカラム名を指定します。
		final String s_linkClm		= getParam( "LINK_CLM"		);	// レコード単位に、クリッカブルリンクを設定するカラム名を指定します。

		useLegend		= getBoolParam( "USE_LEGEND"		);	// カラーの凡例を使用するかどうか[true/false]
		maxLabelWidth	= getIntParam( "MAX_LABEL_WIDTH"	);	// ラベル表記部の最大サイズをpxで指定。何もなければ、可変長サイズ
		maxTimeWidth	= getIntParam( "MAX_TIME_WIDTH"		);	// タイム表記部の最大サイズをpxで指定。
		chartHeight		= getIntParam( "CHART_HEIGHT"		);	// １レコードのチャートの間隔をpxで指定。実際の幅は、CHART_HEIGHT＋MARGIN＊２
		headerHeight	= getIntParam( "HEADER_HEIGHT"		);	// 6.4.6.1 (2016/06/03) ヘッダー部の高さをpxで指定。凡例とヘッダーの高さになります。
		if( headerHeight < 0 ) { headerHeight = chartHeight; }	// 指定が無い場合は、チャート幅と同じにします。
		chartPadding	= getIntParam( "CHART_PADDING"		);	// イメージ作成の 全体テーブルの隙間
		recodeMargin	= getIntParam( "RECODE_MARGIN"		);	// 各レコード、文字等の内部の間隔
		useLastData		= getBoolParam( "USE_LAST_DATA"		);	// 5.6.1.1 (2013/02/08) 行の最後の情報が、継続しているとして使うかどうか[true/false]
		isDebug			= getBoolParam( "debug"				);	// 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします[true/false]。

		tempDir			= getParam( "TEMP_DIR"				);	// 画像ファイルを作成するテンポラリディレクトリ(相対パス)
		tempUrl			= getParam( "TEMP_URL"				);	// 作成した画像ファイルを取得するときに使用するURL(コンテキスト/相対パス)

		headerUpFmt		= getParam( "HEADER_UP_FMT"			);	// 7.1.0.0 (2020/01/20) ヘッダー部分上段の日付フォーマット
		headerDwFmt		= getParam( "HEADER_DW_FMT"			);	// 7.1.0.0 (2020/01/20) ヘッダー部分下段の日付フォーマット

		startDate		= getStr2Date( s_startDate );			// 分単位に変換する。
		startTime		= str2DateTime ;						// 開始日時の時刻情報(分単位)の値。str2DateTime は、getStr2Date(String)メソッド実行後にセットされる。
		timeSpan		= timeSpanHour * 60L ;					// 分単位に変換する。

		final int len = s_labelClms.length;
		if( len > 0 ) {
			labelClmsNo = new int[len];
			for( int i=0; i<len; i++ ) {
				labelClmsNo[i] = getColumnNo( s_labelClms[i] );
			}
		}
		else {
			labelClmsNo = new int[] { KEY_CLMNO };		// 初期値は、キーカラム
		}

		// 指定のカラム名に対するカラム番号を取得。なければ、-1 を設定しておく。
		if( s_colClm  != null ) { colClmNo  = getColumnNo( s_colClm  ); }			// レコードに付ける色を色文字列で指定する場合のカラムNo
		if( s_tipsClm != null ) { tipsClmNo = getColumnNo( s_tipsClm ); }			// レコード単位に、マウスオーバー時のTips表示を行うカラムNo
		if( s_linkClm != null ) { linkClmNo = getColumnNo( s_linkClm ); }			// レコード単位に、クリッカブルリンクを設定するカラムNo

		// 6.0.2.0 (2014/09/19) データがない最後の処理で使用する現在時刻
		final Calendar cal = Calendar.getInstance();
		nowTime = cal.getTimeInMillis() / MILLI_MINUTE - startDate ;		// 6.0.2.0 (2014/09/19) ミリ秒を分に換算、現在時刻(分単位)
	}

	/**
	 * イメージの ＸＹサイズを求め、結果を、maxX,maxY 変数に設定します。
	 *
	 * また、ラベル文字列の最大長が指定されていない場合（maxLabelWidth == -1）最大長を自動計算し、maxLabelWidth変数にセットします。
	 *
	 * maxLabelWidth : -1 の場合は、ラベル文字列の最大長を求めて、この値を計算する。= (recodeMargin*2 + ラベル文字列の最大長)
	 * maxX : イメージの最大横幅(X)方向のサイズpx。chartPadding*2 + maxLabelWidth + maxTimeWidth
	 * maxY : イメージの最大縦幅(Y)方向のサイズpx。chartPadding*2 + (chartHeight+recodeMargin*2)*(レコード数＋ヘッダー数)
	 *
	 * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、<span>タグが付与される値から、spanタグを削除します。
	 * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
	 * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
	 * 
	 * @param	startNo	計算の開始列番号
	 * @param	lastNo	計算の終了列番号
	 */
	private void calcImageSize( final int startNo , final int lastNo ) {
		String oldKeyVal = "";	// 初期値。１回目にキーブレイクさせる。

		final int clmSu = labelClmsNo.length;
		maxClmWidth = new int[clmSu];

		// 6.4.6.1 (2016/06/03) ヘッダー部の高さは、凡例とヘッダー部が対象。
		int rowCnt = 0;						// 6.4.6.1 (2016/06/03) レコード数

		// ラベル領域の長さを各ラベル長を積み上げて計算する。
		if( maxLabelWidth < 0 ) {
			// FontMetrics を取得する為だけの BufferedImage
			final BufferedImage img = new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB);
			final Graphics2D g2 = img.createGraphics();
			final FontMetrics fontM = g2.getFontMetrics();

			// 初期値の計算は、ヘッダーのラベルの幅を使用する。
			for( int col=0; col<clmSu; col++ ) {
				final String lbl = StringUtil.spanCut( getColumnLabel( labelClmsNo[col] ) );		// 5.6.3.1 (2013/04/05) spanタグを削除
//				maxClmWidth[col] = fontM.stringWidth( lbl );										// 最大値の初期値として、ヘッダーラベルの幅をセットしておく。
				// 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
				final String[] xlbls = lbl.split( "\\n|\\\\n" );									// 本物の改行と、"\n" 文字列
				if( xlbls.length > 1 ) {
					useCR = true;
					for( final String xlbl : xlbls ) {
						final int fontW = fontM.stringWidth( xlbl );
						if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }				// 分割したラベルで、最大値を求める。
					}
				}
				else {
					maxClmWidth[col] = fontM.stringWidth( lbl );									// 最大値の初期値として、ヘッダーラベルの幅をセットしておく。
				}
			}

			for( int row=startNo; row<lastNo; row++ ) {
				// キーブレイク判定。キーブレイクは、一番初めから来る。
				final String keyVal = getValue( row,KEY_CLMNO );
				if( !oldKeyVal.equals( keyVal ) ) {
					oldKeyVal = keyVal;
					rowCnt++;				// レコード数

					// ラベルは、キーブレイク時の値のみ採用する。
					for( int col=0; col<clmSu; col++ ) {
						final String lbl = StringUtil.spanCut( getValueLabel( row,labelClmsNo[col] ) );		// 5.6.3.1 (2013/04/05) spanタグを削除
//						final int fontW = fontM.stringWidth( lbl );
//						if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }

						// 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
						final String[] xlbls = lbl.split( "\\n|\\\\n" );									// 本物の改行と、"\n" 文字列
						if( xlbls.length > 1 ) {
							useCR = true;
							for( final String xlbl : xlbls ) {
								final int fontW = fontM.stringWidth( xlbl );
								if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }					// 分割したラベルで、最大値を求める。
							}
						}
						else {
							final int fontW = fontM.stringWidth( lbl );
							if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }
						}
					}
				}
			}
			g2.dispose();

			// 最大ラベル幅は、各ラベルの最大値＋（マージン＊２）＊カラム数
			maxLabelWidth = recodeMargin * 2 * clmSu ;
			for( int col=0; col<clmSu; col++ ) {
				maxLabelWidth += maxClmWidth[col];
			}
		}
		else  {
			for( int row=startNo; row<lastNo; row++ ) {
				// キーブレイク判定。キーブレイクは、一番初めから来る。
				final String keyVal = getValue( row,KEY_CLMNO );
				if( !oldKeyVal.equals( keyVal ) ) {
					oldKeyVal = keyVal;
					rowCnt++;				// レコード数
				}
			}

			// 最大ラベル幅は、均等割り付け。端数は無視（どうせ、ラベル部は、maxLabelWidth で計算するので。）
			final int clmWidth = ( maxLabelWidth - recodeMargin * 2 * clmSu ) / clmSu ;
			for( int col=0; col<clmSu; col++ ) {
				maxClmWidth[col] = clmWidth;
			}
		}

		maxX = chartPadding*2 + maxLabelWidth + maxTimeWidth ;
		// 6.4.6.1 (2016/06/03) ヘッダー部の高さは、凡例とヘッダー部を考慮して、加算する。
		maxY = chartPadding*2 + (headerHeight+recodeMargin*2)*(useLegend ? 2 : 1) + (chartHeight+recodeMargin*2)*rowCnt ;
	}

	/**
	 * ヘッダー情報のイメージを作成します。
	 *
	 * 全体の枠もここで作成しておきます。
	 * イメージは、キーカラムのラベルと、時間軸になります。時間軸は縦方向にすべて線を引きます。
	 * 時間軸の間隔は、timeScale によって、切り替わります。
	 * 凡例を使う場合（useLegend=true）は、引数の FlgColorMap を利用して作成します。
	 *
	 * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、<span>タグが付与される値から、spanタグを削除します。
	 * @og.rev 5.6.5.0 (2013/06/07) 年月日情報を表示します。なお、日単位の場合は、年月は省略します。
	 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
	 * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
	 * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
	 * @og.rev 7.0.5.1 (2019/09/27) Date → Calendar 変更、日付表示方法見直し。
	 * @og.rev 7.0.7.1 (2019/12/24) 日付の箇所が、時間になっていた。
	 * @og.rev 7.1.0.0 (2020/01/20) ヘッダー部分の日付フォーマット指定の追加
	 *
	 * @param	g2			描画するGraphics2Dオブジェクト
	 * @param	timeScale	時間(分)当たりのピクセル数
	 * @param	colMap		状況コードに対応したカラーマップ
	 */
	private void imageHeaderPaint( final Graphics2D g2 , final double timeScale , final FlgColorMap colMap ) {

		int posY1 = chartPadding  ;
		int posY2 = chartPadding+headerHeight+recodeMargin ;		// 6.4.6.1 (2016/06/03) ヘッダー部の高さ

		// 凡例を使う場合
		if( useLegend && colMap != null ) {
			// 凡例を並べる間隔を求める。
			final FontMetrics fontM = g2.getFontMetrics();
			final int maxSize = fontM.stringWidth( colMap.getMaxLengthLabel() ) ;		// 文字列の最大長ラベルの幅
			final int imgW  = headerHeight ;			// 凡例■の幅(高さと同じにしているので真四角) 、6.4.6.1 (2016/06/03) 
			final int mgnW  = recodeMargin ;			// 凡例■から文字までの間
			final int spanW = maxSize + recodeMargin ;	// 凡例■から凡例■までの間隔。文字最大長＋α

			int legX  = chartPadding ;

			// 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
			for( final FlgColorObj obj : colMap.values() ) {
				g2.setColor( obj.COL );											// 凡例■の色
				g2.fillRect( legX , posY1+recodeMargin , imgW , headerHeight );	// (実際の状態)左端x,上端y,幅w,高さh、6.4.6.1 (2016/06/03) 

				legX += imgW + mgnW ;
				g2.setColor( LABEL_COLOR );
				g2.drawString( obj.LBL , legX , posY2 );	// 文字列,ベースラインのx座標,y座標

				legX += spanW ;
			}
			posY1 += headerHeight+recodeMargin*2 ;	// １レコード分の高さを加算しておく。、6.4.6.1 (2016/06/03) 
			posY2 += headerHeight+recodeMargin*2 ;	// １レコード分の高さを加算しておく。、6.4.6.1 (2016/06/03) 
		}

		// まずは、全体の枠線の描画
		g2.setColor( LABEL_COLOR );
		g2.drawRect( chartPadding, posY1, maxX-chartPadding*2, maxY-posY1-chartPadding );		// 左端,上端,幅,高さ

		// ヘッダーのラベル分だけ、繰返し描画する。
		final int clmSu = labelClmsNo.length;
		int posX  = chartPadding ;		// ラベル文字列の書き出し位置の初期値
		for( int col=0; col<clmSu; col++ ) {
			// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
			final String lbl = StringUtil.spanCut( getColumnLabel( labelClmsNo[col] ) );		// 5.6.3.1 (2013/04/05) spanタグを削除
//			g2.drawString( lbl , posX + recodeMargin, posY2 );			// 文字列,ベースラインのx座標,y座標
			// 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
			if( useCR ) {
				final String[] xlbls = lbl.split( "\\n|\\\\n" );								// 本物の改行と、"\n" 文字列
				for( int i=0; i<xlbls.length; i++ ) {
					final int posY3 = posY2 - (xlbls.length-1-i)*headerHeight/xlbls.length;		// 改行ラベルを、均等に枠内に分割します。
					g2.drawString( xlbls[i] , posX + recodeMargin, posY3  );					// 文字列,ベースラインのx座標,y座標
				}
			}
			else {
				g2.drawString( lbl , posX + recodeMargin, posY2 );		// 文字列,ベースラインのx座標,y座標
			}

			posX += recodeMargin*2 + maxClmWidth[col] ;					// 次の書き出し位置は、文字最大長＋マージン*2
			g2.drawLine( posX, posY1, posX, maxY-chartPadding );		// 始点(x 座標,y 座標),終点(x 座標,y 座標)
		}

		final int step = TimeScaleStep.getStep( timeScale );			// 時間スケールに対応したSTEP数

		// 日付ヘッダー部の描画のためのカレンダー
		final Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis( startDate * MILLI_MINUTE );				// 6.0.2.0 (2014/09/19) ミリ秒に換算した分

//		final int week = cal.get( Calendar.DAY_OF_WEEK );				// 開始曜日 7.0.5.1 (2019/09/27) 削除

//		final String fmt = step < MINUTE_OF_DAY ? "yyyy/MM/dd(EE)" : "dd" ;		// 上段フォーマットは、時間ベースの場合は、"yyyy/MM/dd(EE)" 、日ベースの場合は、"dd"
//		final String fmt1 = step >= MINUTE_OF_DAY ? "M月" : "H/d(EE)" ;			// 7.0.5.1 (2019/09/27) 判定の反転と上段ﾌｫｰﾏｯﾄ変更

		// 7.1.0.0 (2020/01/20) ヘッダー部分の日付フォーマット指定の追加
//		final String fmt1 = step >= MINUTE_OF_DAY ? "M月" : "M/d(EE)" ;			// 7.0.7.1 (2019/12/24) 日付の箇所が、時間になっていた。
//		final String fmt2 = step >= MINUTE_OF_DAY ? "dEE" : "H:mm" ;			// 7.0.5.1 (2019/09/27) 下段ﾌｫｰﾏｯﾄ追加
		final String fmt1 = headerUpFmt != null ? headerUpFmt : step >= MINUTE_OF_DAY ? "M月" : "M/d(EE)" ;		// 7.1.0.0 (2020/01/20)
		final String fmt2 = headerDwFmt != null ? headerDwFmt : step >= MINUTE_OF_DAY ? "dEE" : "H:mm" ;		// 7.1.0.0 (2020/01/20)

		final DateFormat format1 = new SimpleDateFormat( fmt1,Locale.JAPAN );	
		final DateFormat format2 = new SimpleDateFormat( fmt2,Locale.JAPAN );	// 7.0.5.1 (2019/09/27) 下段ﾌｫｰﾏｯﾄ追加

		// グラフ領域の日付ヘッダー部の描画
		g2.setStroke( DSAH_STROK );				// 日付部は、破線
		posX = chartPadding+maxLabelWidth ;		// グラフ領域は、chartPadding+maxLabelWidth から。
		int oldKey = -1;						// 7.0.5.1 (2019/09/27) ﾌﾞﾚｲｸさせるｷｰ（日や月）の旧の値
		for( int tm=0; tm<timeSpan; tm+=step ) {
			int offset = headerHeight / 2 + recodeMargin;	// ヘッダーの表示基準のオフセット（チャート幅の半分）6.4.6.1 (2016/06/03)
			// 上段：ヘッダー出力 7.0.5.1 (2019/09/27) Date → Calendar 変更
			final Calendar calH = Calendar.getInstance();
			calH.setTimeInMillis( (startDate + tm) * MILLI_MINUTE );
			// 7.0.5.1 (2019/09/27) ステップが日単位より大きい場合は、月のﾌﾞﾚｲｸでﾍｯﾀﾞｰ出力します。
			final int newKey = step >= MINUTE_OF_DAY ? calH.get( Calendar.MONTH ) : calH.get( Calendar.DATE );
			if( oldKey != newKey ) {
				oldKey = newKey;
				g2.drawString( format1.format( calH.getTime() ) , posX + recodeMargin , posY2-offset );	// 文字列,ベースラインのx座標,y座標
				offset = 0;		// ヘッダーを表示する場合のみ上まで線を引く。
			}
//			if( tm % MINUTE_OF_DAY == 0 ) {			// 6.0.2.0 (2014/09/19) １日が何分
//				final Date dt = new Date( (startDate + tm) * MILLI_MINUTE );						// 6.0.2.0 (2014/09/19) ミリ秒に換算した分
//				g2.drawString( format1.format( dt ) , posX + recodeMargin , posY2-offset );			// 文字列,ベースラインのx座標,y座標
//				offset = 0;		// ヘッダーを表示する場合のみ上まで線を引く。
//			}

			// getTime2Str に 7.0.5.1 (2019/09/27) 日付情報を渡します。
//			g2.drawString( getTime2Str(startTime+tm,step,week) , posX + recodeMargin , posY2 );		// 文字列,ベースラインのx座標,y座標
			g2.drawString( format2.format( calH.getTime() ) , posX + recodeMargin , posY2 );		// 文字列,ベースラインのx座標,y座標
			g2.drawLine( posX, posY1+offset, posX, maxY-chartPadding );								// 始点(x 座標,y 座標),終点(x 座標,y 座標)

			posX += (int)(step*timeScale);
		}
	}

	/**
	 * 時間スケールに対応したSTEP数を管理するための内部クラス
	 *
	 * 時間ヘッダーを表示する場合、ある程度意味のある間隔でラベル表示したいと思います。
	 * 全体の描画領域の長さと、時間当たりのスケールファクター（ピクセル数）から、
	 * ラベルの描画間隔を求めます。
	 * 意味のある間隔は、STEPS で定義し、10分,30分,60分,1/4日,1/2日,1日 まで定義しています。
	 * 一番大きな単位以上の場合は、その最大単位の整数倍を返します。
	 *
	 * 一時間当たりの表示幅を、MIN_PX としていますので、この値以下の間隔では描画されません。
	 * 初期値は、600px を 24時間表示できる 600px/24h = 25px にしています。
	 */
	private static final class TimeScaleStep {
													// 分   分   時   1/4   1/2  １日
		private static final int[] STEPS = new int[] { 10 , 30 , 60 , 360 , 720 , 1440 };
		private static final int MIN_PX = 25;		// スケールに対する最小値

		/**
		 * オブジェクトを作らせない為の、private コンストラクタ
		 */
		private TimeScaleStep() {}

		/**
		 * 時間を意味のある範囲の整数として返します。
		 *
		 * 全体の描画領域の長さと、時間当たりのスケールファクター（ピクセル数）から、
		 * 10分,30分,60分,1/4日,1/2日,1日 までの整数値で返します。
		 * 一番大きな単位以上の場合は、その最大単位の整数倍を返します。
		 *
		 * @param	timeScale	時間(分)当たりのピクセル数
		 * @return	時間スケールに対応した意味のある範囲の整数
		 */
		public static int getStep( final double timeScale ) {
			final int tmStep = (int)Math.ceil(MIN_PX/(timeScale));	// 整数切り上げ

			for( int i=0; i<STEPS.length; i++ ) {
				if( tmStep <= STEPS[i] ) { return STEPS[i]; }	// 配列の数字に切り上げ
			}

			// 未設定の場合は、最上位の値の整数倍に切り上げ
			return (int)Math.ceil( (double)tmStep / STEPS[STEPS.length-1] ) * STEPS[STEPS.length-1];
		}
	}

	/**
	 * 状況コード、ラベル、色を管理するための内部クラス
	 *
	 * 状況に応じたコード、ラベル、色を管理します。
	 * これは、getColor(状況コード,ラベル) または、getColor(状況コード,ラベル,色文字列) で
	 * 要求された情報を内部で管理し、同じコードの場合に同じ色を返します。
	 * また、凡例作成用に、最も文字数が長いラベルを管理します。
	 * 色文字列を指定した場合でも、最初に要求された状況コードに対応する色を返します。
	 * これは、同一状況コードで色違いを作成することができないことを意味します。
	 * 色文字列を指定しない場合は、内部の色配列から、順番に色を割り当てます。
	 * 色を割り当てる順番は、状況コードの発生順です。よって、検索条件によって、
	 * 状況コードの現れる順番が異なると、色も毎回異なることになります。
	 *
	 * 自動割り当ての色は、BLUE,CYAN,GRAY,GREEN,LIGHT_GRAY,MAGENTA,DARK_GRAY,ORANGE,PINK,RED,YELLOW 
	 * となっており、それを超えると、RGBをランダムに発生させて色を作成します。
	 * よって、どのような色になるかは全くわかりません。
	 *
	 * @og.rev 6.4.3.3 (2016/03/04) CLR_ARY等、カラー関連処理を、ColorMapクラスへ移動します。
	 */
	private static final class FlgColorMap {
		// 6.4.3.3 (2016/03/04) ColorMapクラスへ移動
		private final Map<String,FlgColorObj> colMap = new TreeMap<>();										// 6.3.9.0 (2015/11/06)

		private int		lastCnt		;
		private String	maxLabel	= "" ;		// 最大長のラベル
		private int		maxlen		= -1 ;		// 最大長のラベルのlength()

		/**
		 * 状況コードに対応した色オブジェクトを返します。
		 *
		 * 状況コードが初めて指定された場合は、順番に内部の色を割り当てます。
		 * また、その時のラベルも管理します。ラベルと色のセットは、凡例作成時に利用されます。
		 *
		 * 自動割り当ての色は、BLUE,CYAN,GRAY,GREEN,LIGHT_GRAY,MAGENTA,DARK_GRAY,ORANGE,PINK,RED,YELLOW 
		 * となっており、それを超えると、RGBをランダムに発生させて色を作成します。
		 * よって、どのような色になるかは全くわかりません。
		 *
		 * @param	fgj	状況コード
		 * @param	lbl	状況コードのラベル
		 * @return	状況コードに対応した色オブジェクト
		 */
		public Color getColor( final String fgj,final String lbl ) {
			return getColor( fgj,lbl,null );
		}

		/**
		 * 状況コードに対応した色オブジェクトを返します。
		 *
		 * 状況コードが初めて指定された場合は、引数の色文字列の色を割り当てます。
		 * また、その時のラベルも管理します。ラベルと色のセットは、凡例作成時に利用されます。
		 *
		 * 色文字列を指定した場合でも、最初に要求された状況コードに対応する色を返します。
		 * これは、同一状況コードで色違いを作成することができないことを意味します。
		 * 色文字列 が null の場合は、自動割り当てのメソッドと同じです。
		 * よって、色文字列の指定と、自動割り当てが同時に発生すると、異なる状況コードで
		 * 同じ色が指定される可能性がありますので、混在して使用しないでください。
		 *
		 * @og.rev 6.0.2.1 (2014/09/26) StringUtil → ColorMap
		 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
		 * @og.rev 6.4.3.3 (2016/03/04) ColorMapクラスへ移動。それに関連する修正。
		 *
		 * @param	fgj		状況コード
		 * @param	lbl		状況コードのラベル
		 * @param	colStr	状況コードに対応した色文字列(nullの場合は、自動割り当て)
		 * @return	状況コードに対応した色オブジェクト
		 * @og.rtnNotNull
		 */
		public Color getColor( final String fgj,final String lbl,final String colStr ) {
			// 6.3.9.1 (2015/11/27) A method should have only one exit point, and that should be the last statement in the method.(PMD)
			final Color rtn ;
			if( fgj == null ) { rtn = LABEL_COLOR; }
			else {
				if( lbl != null ) {
					final int len = lbl.length();
					if( len > maxlen ) { maxLabel = lbl; maxlen = len; }
				}

				// Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
				// colMap からは、FlgColorObj が戻されるので、COL 属性を取得する。
				final String cloCnt = colStr == null ? String.valueOf( lastCnt++ ) : colStr ;
				rtn = colMap.computeIfAbsent( fgj , k -> new FlgColorObj( lbl , cloCnt ) ).COL;
			}

			return rtn ;
		}

		/**
		 * 内部で管理している、ラベル(String)と色オブジェクト(Color)の コレクションを返します。
		 *
		 * 内部で管理しているコレクションです。
		 * このコレクションは、状況コードでソートされています。
		 * コレクションの中身は、オブジェクト配列となっており、[0]は、String型のラベル、[1]は、
		 * Color型の色です。
		 *
		 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
		 *
		 * @return	ラベル(String)と色オブジェクト(Color)の コレクション
		 */
		public Collection<FlgColorObj> values() {
			return colMap.values();
		}

		/**
		 * 登録されたラベル文字列で、最も文字数が長いラベルを返します。
		 *
		 * 凡例で、ラベルの最大長を求めるのに利用できます。
		 * ただし、簡易的に、length() 計算しているだけなので、英語、日本語混在や、
		 * プロポーショナルフォント使用時の厳密な最大長の文字列ではありません。
		 *
		 * @return	最も文字数が長いラベル
		 */
		public String getMaxLengthLabel() { return maxLabel; }
	}

	/**
	 * FlgColorMapで管理している内部クラス
	 *
	 * フラグに対して、Object[] 配列の [0]:ラベル、[1]:カラー で管理してましたが、
	 * きちんとString と Color で管理します。そのための内部クラスです。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
	 */
	private static final class FlgColorObj {
		private final String LBL ;
		private final Color  COL ;

		/**
		 * ラベルとカラーを指定したインストラクター
		 *
		 * @param	lbl	ラベル
		 * @param	col	カラー
		 */
		FlgColorObj( final String lbl , final Color col ) {
			LBL = lbl ;
			COL = col ;
		}

		/**
		 * ラベルとカラー文字列を指定したインストラクター
		 *
		 * カラー文字列は、ColorMap で定義されている文字列です。
		 *
		 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
		 *
		 * @param	lbl		ラベル
		 * @param	colStr	カラー文字列
		 */
		FlgColorObj( final String lbl , final String colStr ) {
			LBL = lbl ;
			COL = ColorMap.getColorInstance( colStr );
		}

		/**
		 * ラベルとカラー番号を指定したインストラクター
		 *
		 * カラー番号は、ColorMap で定義されている番号です。
		 *
		 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
		 *
		 * @param	lbl	ラベル
		 * @param	no	カラー番号
		 */
		FlgColorObj( final String lbl , final int no ) {
			LBL = lbl ;
			COL = ColorMap.getColorInstance( no );
		}

		/**
		 * ラベルを返します。
		 *
		 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
		 *
		 * @return	ラベル
		 */
		/* default */ String getLabel() { return LBL; }

		/**
		 * カラーを返します。
		 *
		 * @return	カラー
		 */
		/* default */ Color  getColor() { return COL; }
	}

	/**
	 * 日時文字列を数字に変換します。
	 *
	 * 日時文字列は、yyyyMMdd または、yyyyMMddHHmmss 形式とします。
	 * これを、エポックタイムからの経過時間の 分単位の値を求めます。
	 * 具体的には、Calendar オブジェクトの getTimeInMillis() の結果を、
	 * 60000 で割り算した値を作成します。
	 * よって、Calendar オブジェクト作成時も、秒の単位は切り捨てます。
	 * 引数が null の場合は、現在時刻より、作成します。
	 *
	 * @param	val  	日時文字列の値(yyyyMMdd または、yyyyMMddHHmmss 形式 など)
	 *
	 * @return	日時文字列を分換算した数字
	 */
	private long getStr2Date( final String val ) {
		final Calendar cal = Calendar.getInstance();
		str2DateTime = 0;
		if( val == null ) {
			cal.set( Calendar.HOUR_OF_DAY, 0 );		// 5.3.5.0 (2011/05/01) 時間の解決規則が適用されるため、｢時｣だけは、setメソッドで 0 にセットする。
			cal.clear( Calendar.MINUTE );
			cal.clear( Calendar.SECOND );
			cal.clear( Calendar.MILLISECOND );
		}
		else if( val.length() == 8 ) {
			cal.clear();
			cal.set( Integer.parseInt( val.substring( 0,4 ) ) ,			// 年
					 Integer.parseInt( val.substring( 4,6 ) ) - 1,		// 月(0から始まる)
					 Integer.parseInt( val.substring( 6,8 ) )  			// 日
			);
		}
		else {
			cal.clear();
			cal.set( Integer.parseInt( val.substring( 0,4 ) ) ,			// 年
					 Integer.parseInt( val.substring( 4,6 ) ) - 1,		// 月(0から始まる)
					 Integer.parseInt( val.substring( 6,8 ) ) ,			// 日
					 Integer.parseInt( val.substring( 8,10 ) ) ,		// 時
					 Integer.parseInt( val.substring( 10,12 ) ) 		// 分
			);
			str2DateTime = Integer.parseInt( val.substring( 8,10 ) ) * 60 + Integer.parseInt( val.substring( 10,12 ) ) ;
		}
		return cal.getTimeInMillis() / MILLI_MINUTE ;		// 6.0.2.0 (2014/09/19) ミリ秒に換算した分
	}

	/**
	 * 数字(分)を時間文字列に変換します。
	 *
	 * 480 は、"08" に、1260 は、"21" に変換します。
	 * 引数の時間は、分を表す整数です。２４時間表記であれば、0 ～ 1440 の範囲で収まりますが、
	 * 期間が長い場合は、その値を超えます。また、２４時間を超える場合は、0 に戻ります。
	 * 文字列にする場合の最小単位は、(時)なので、60(分)で割り算して、余は、切り捨てます。
	 * step は、60(分)単位の表示時に飛ばす数です。step=60 なら、60(分)単位、step=120 なら、120(分)
	 * 単位となります。stepが、1440 以下の場合は、そのまま、24時間表記で構いませんが、
	 * それを超えると時間ではなく、日数表記に切り替わります。
	 *
	 * @og.rev 5.6.5.0 (2013/06/07) 月単位の場合は、曜日を表示します。
	 * @og.rev 7.0.5.1 (2019/09/27) Date → Calendar 変更、日付表示方法見直し。
	 *
	 * @param	timeVal	引数の時間整数(分)
//	 * @param	step	60分単位のステップ数( 10,30,60,720,1440 単位となるように調整)
//	 * @param	week	カレンダクラスの曜日フィールド(DAY_OF_WEEK)
	 *
	 * @return	数字を時間文字列に変換した結果( "08" など)
	 * @og.rtnNotNull
	 */
//	private String getTime2Str( final int timeVal, final int step, final int week ) {	// 7.0.5.1 (2019/09/27)
	private String getTime2Str( final int timeVal ) {
		final StringBuilder rtn = new StringBuilder( BUFFER_MIDDLE );

//		if( step >= MINUTE_OF_DAY ) {								// 6.0.2.0 (2014/09/19) １日が何分
//			final int dtm = timeVal / MINUTE_OF_DAY ;				// 日の整数値
//			rtn.append( DAY_OF_WEEK_JA[ ( dtm + week ) % W_7 ] );	// 曜日を表示
//		}
//		else {
			final int htm = (timeVal / M_60) % H_24 ;		// 時(24時間制)
	//		if( htm < DEC ) { rtn.append( '0' ); }			// 6.0.2.5 (2014/10/31) 1桁の場合は、先頭に0を付ける	7.0.5.1 (2019/09/27) 先頭の 0 は外す。
			rtn.append( htm );

//			if( step < M_60 ) {								// 7.0.5.1 (2019/09/27) 時間表示は、常に分まで出す。
				final int mtm = timeVal % M_60 ;			// 分(６０分制)
				rtn.append( ':' );							// 6.0.2.5 (2014/10/31) char を append する。
				if( mtm < DEC ) { rtn.append( '0' ); }		// 6.0.2.5 (2014/10/31) char を append する。
				rtn.append( mtm );
//			}
//		}

		return rtn.toString();
	}

	/**
	 * 表示項目の編集(並び替え)が可能かどうかを返します。
	 *
	 * @return	表示項目の編集(並び替え)が可能かどうか(false:不可能)
	 */
	@Override
	public boolean isEditable() {
		return false;
	}
}
