    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.plugin.view;
    017
    018import org.opengion.hayabusa.common.HybsSystemException;
    019import org.opengion.fukurou.util.StringUtil;
    020import org.opengion.fukurou.util.ColorMap;                              // 6.0.2.1 (2014/09/26)
    021import org.opengion.fukurou.system.OgBuilder ;                  // 6.4.4.1 (2016/03/18)
    022
    023import java.util.Calendar;
    024import java.util.Map;
    025import java.util.TreeMap;
    026import java.util.Collection;
    027import java.util.Locale ;
    028import java.util.Date;
    029import java.text.DateFormat;
    030import java.text.SimpleDateFormat;
    031
    032import java.awt.Graphics2D;
    033import java.awt.Color;
    034import java.awt.FontMetrics;
    035import java.awt.Stroke;
    036import java.awt.BasicStroke;
    037import java.awt.image.BufferedImage;
    038
    039import javax.imageio.ImageIO;
    040import java.io.File;
    041import java.io.IOException;
    042
    043/**
    044 * キー、日時、状況コードを持つ稼働状況の表示を行うクラスです。
    045 *
    046 * パラメータが必要な場合は、ViewTimeBarParamTag を使用してください。
    047 *
    048 * パラメータが設定されていない場合は、ViewForm_ImageTimeBar の初期値が使用されます。
    049 * (パラメータを使用するには、viewタグのuseParam 属性をtrueに設定する必要があります。)
    050 *
    051 * SELECT文は、キー、日時、状況コードが、必須項目で、カラムの並び順は、完全に固定です。
    052 * よって、カラム位置を指定する必要はありませんが、SELECT文を自由に設定することも
    053 * 出来ませんので、ご注意ください。
    054 * この固定化に伴い、WRITABLE 指定も使用できません。（そもそも書き込み不可です）
    055 * それ以降のカラムについては、内部処理としては、使用していません。
    056 * ただし、パラメータで、カラー色指定、ラベル表記部、イメージ重ね合わせ、
    057 * ポップアップ表記、リンク表記に使えます。
    058 *
    059 * データの並び順(ORDER BY)も、キー、日時順にしてください。
    060 * データは、キー単位に１レコード作成されます。（キーブレイク）その間、日時順に
    061 * データを処理します。
    062 *
    063 * データの表示は、今のレコードの日時から、次のレコードの日時までを一つの状態と
    064 * して表します。今のレコードを表示するには、次のレコードが必要になります。
    065 * 画面表示は、表示開始日時(minStartTime) から 表示期間(timeSpan)分を表示します。
    066 * 通常、開始時刻は、表示開始時刻より前より始まり、次のレコードで、終了時刻が決定
    067 * されます。最後のデータは、期間満了まで続いていると仮定されます。
    068 * データが存在しないのであれば、「存在しないデータ」を作成してください。
    069 *
    070 * ImageTimeBar では、キーでまとめた値について、各状況コードをカラー化し、積み上げ
    071 * 帯グラフ形式でPNG画像化します。
    072 * この画像を、読み込む HTML を出力することで、画面上に、積み上げ帯グラフを表示します。
    073 * 状況コードに対応する色は、標準では自動作成ですが、外部から色文字列(colorClm)を与えることで
    074 * 自由に指定する事も可能です。
    075 *
    076 * ポップアップ表記(tipsClm)、リンク表記(linkClm)は、この画像に対するエリア指定タグを出力する事で実現します。
    077 * 画像ファイルは、全データに対して、１画像だけなので、サイズは大きくなりますが、１レコード
    078 * 単位に画像を作成しないため、レスポンスは向上します。
    079 * それぞれ、viewMarker , viewLink を利用することが可能です。特に、リンク表記(linkClm) については、
    080 * linkタグの hrefTarget 属性を true に設定することで適用できます。
    081 *
    082 * 画像ファイルは、java.io.File.createTempFile( File ) で作成するため、JavaVM(=Tomcat)が
    083 * 正常終了するときに、削除されます。異常終了時には残りますが、temp フォルダを定期的に
    084 * 整理すれば、それほど大量のファイルが残ることはないと思われます。
    085 *
    086 * データは、イベント発生時に作成されると仮定しています。つまり、書き込まれた日時から、
    087 * 状況コードに対応する状況が発生し、次の状況違いのレコードまで継続していると考えます。
    088 * よって、データを途中で切り出す場合、切り出す範囲の前の状態が必要になります。
    089 * 一番最初の状態は、"不明" として扱います。（空欄＝白色）
    090 *
    091 * @og.rev 5.5.5.6 (2012/08/31) 新規追加
    092 * @og.group 画面表示
    093 *
    094 * @version  4.0
    095 * @author       Kazuhiko Hasegawa
    096 * @since    JDK5.0,
    097 */
    098public class ViewForm_ImageTimeBar extends ViewForm_HTMLTable {
    099        /** このプログラムのVERSION文字列を設定します。   {@value} */
    100        private static final String VERSION = "6.9.9.1 (2018/08/27)" ;
    101
    102        private static final Color LABEL_COLOR  = Color.BLACK;          // ラベル記述時の色
    103        private static final Color NULL_COLOR   = Color.WHITE;          // 5.6.1.1 (2013/02/08) 不明(空欄)時の色
    104
    105        private static final int        M_60            = 60;                           // 6.1.0.0 (2014/12/26) refactoring
    106        private static final int        H_24            = 24;                           // 6.1.0.0 (2014/12/26) refactoring
    107        private static final int        W_7                     = 7;                            // 6.1.0.0 (2014/12/26) refactoring
    108        private static final int        DEC                     = 10;                           // 6.1.0.0 (2014/12/26) refactoring
    109
    110        private static final int        MINUTE_OF_DAY = 24 * 60 ;               // 6.0.2.0 (2014/09/19) １日が何分 なのか(=1440)
    111        private static final long       MILLI_MINUTE  = 60 * 1000L;             // 6.0.2.0 (2014/09/19) ミリ秒に換算した分。(=60000L)
    112
    113        // 5.6.5.0 (2013/06/07) 曜日データを配列で持っておきます。
    114        // 6.4.1.1 (2016/01/16) DAY_OF_WEEK_ja → DAY_OF_WEEK_JA refactoring
    115        private static final String[] DAY_OF_WEEK_JA = { "土","日","月","火","水","木","金","土" };     // [0]="土" は、1～7 の余計算で、 7=0 になる為。
    116
    117        private long startDate          ;       // タイムテーブルの表示開始日時から求めた long 値(1=分単位)
    118        private long timeSpan           ;       // タイムテーブルの表示期間。元は時間指定であるが、分単位で持つ。(1=分単位)
    119
    120        private boolean useLegend       ;       // カラーの凡例を使用するかどうか[true/false]
    121        private int maxLabelWidth       ;       // ラベル表記部の最大サイズをpxで指定。何もなければ、可変長サイズ
    122        private int maxTimeWidth        ;       // タイム表記部の最大サイズをpxで指定。
    123        private int chartHeight         ;       // １レコードのチャートの間隔をpxで指定。実際の幅は、CHART_HEIGHT＋MARGIN＊２
    124        private int headerHeight        ;       // 6.4.6.1 (2016/06/03) チャートのヘッダー部の高さ(初期値は、チャートの間隔:chartHeight)
    125        private int chartPadding        ;       // イメージ作成の 全体テーブルの隙間
    126        private int recodeMargin        ;       // 各レコード、文字等の内部の間隔
    127        private boolean useLastData     ;       // 5.6.1.1 (2013/02/08) 行の最後の情報が、継続しているとして使うかどうか[true/false]
    128        private boolean isDebug         ;       // 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします[true/false]。
    129
    130        private String tempDir          ;       // 画像ファイルを作成するテンポラリディレクトリ(相対パス)
    131        private String tempUrl          ;       // 作成した画像ファイルを取得するときに使用するURL(コンテキスト/相対パス)
    132
    133        // SELECT文は、キー、日時、状況コードが、必須項目
    134        // 6.4.1.1 (2016/01/16) keyClmNo → KEY_CLMNO , dyClmNo → DY_CLMNO , fgjClmNo → FGJ_CLMNO refactoring
    135        private static final int        KEY_CLMNO       = 0;    // ヘッダー1：キーカラムNo
    136        private static final int        DY_CLMNO        = 1;    // ヘッダー2：日時カラムNo
    137        private static final int        FGJ_CLMNO       = 2;    // ヘッダー3：状況コードカラムNo
    138
    139        private int[] labelClmsNo       ;                                       // 初期値は、キーカラム
    140        private int[] maxClmWidth       ;                                       // labelClms 単位の最大文字列長
    141        private int   colClmNo          = -1;                           // カラーコード直接指定する場合に色文字列を指定するカラムNo
    142        private int   tipsClmNo         = -1;                           // マウスオーバー時のTips表示を行うカラムNo
    143        private int   linkClmNo         = -1;                           // クリッカブルリンクを設定するカラムNo
    144
    145        private int str2DateTime        ;       // getStr2Date(String)処理をおこなった時の、引数の時刻情報(分単位)をセットするテンポラリ変数。
    146        private int startTime           ;       // startDate の時刻情報。上記処理で、startDate を getStr2Date(String) 処理したときの値を持っておくための変数。
    147        private long nowTime            ;       // 6.0.2.0 (2014/09/19) データがない最後の処理で使用する現在時刻
    148
    149        // 6.3.9.0 (2015/11/06) Variables should start with a lowercase character(PMD)
    150        private int maxX        ;       // イメージの最大横幅(X)方向のサイズpx。chartPadding*2 + maxLabelWidth + maxTimeWidth ;
    151        private int maxY        ;       // イメージの最大縦幅(Y)方向のサイズpx。chartPadding*2 + (headerHeight+recodeMargin*2)*2 + (chartHeight+recodeMargin*2)*(rowCnt-2);
    152
    153        // 6.9.9.1 (2018/08/27) 必要なのかどうか不明。｢ラベル領域の長さ求めで、改行が含まれる場合の計算対応｣ が必要なのかを判断するﾌﾗｸﾞ。処理速度向上用
    154        private boolean useCR   ;
    155
    156        // imageHeaderPaintメソッドで使用する 破線の定義
    157        private static final Stroke DSAH_STROK = new BasicStroke(
    158                                1.0f                                            , // BasicStroke の幅
    159                                BasicStroke.CAP_BUTT            , // 両端の装飾
    160                                BasicStroke.JOIN_MITER          , // 輪郭線セグメントの接合部の装飾
    161                                1.0f                                            , // 接合トリミングの制限値
    162                                new float[] {2.0f, 1.0f}        , // 破線パターンを表す配列
    163                                0.0f                                              // 破線パターン開始位置のオフセット
    164        );
    165
    166        /**
    167         * DBTableModel から HTML文字列を作成して返します。
    168         * startNo(表示開始位置)から、pageSize(表示件数)までのView文字列を作成します。
    169         * 表示残りデータが pageSize 以下の場合は,残りのデータをすべて出力します。
    170         *
    171         * @og.rev 5.6.1.0 (2013/02/01) tips や link は、ひとつ前のデータで作成する必要がる為、最初の1件目は、処理しないように修正
    172         * @og.rev 5.6.1.1 (2013/02/08) 初期値の色を、直接の値ではなく、static final で定義された色を使用する。色自体は変えていません
    173         * @og.rev 5.6.1.1 (2013/02/08) useLastData 追加
    174         * @og.rev 5.6.3.0 (2013/04/01) tipsのシングルクォーテーション のエスケープ
    175         * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、&lt;span&gt;タグが付与される値から、spanタグを削除します。
    176         * @og.rev 6.0.2.0 (2014/09/19) データが存在しない最後の処理で、現在時刻まで、継続しているとします。
    177         * @og.rev 6.4.2.0 (2016/01/29) alt属性にtitle属性を追記。
    178         * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
    179         * @og.rev 6.4.4.2 (2016/04/01) 引数を、Supplierクラスに変更して、結果を複数指定できるようにします。
    180         * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    181         * @og.rev 6.4.7.1 (2016/06/17) useLastData属性は、最後の処理のみに適用する。
    182         * @og.rev 6.4.8.2 (2016/07/08) 未来時刻の場合に、現在時刻まで巻き戻って塗りつぶしているので、未来時刻の判定を追加。
    183         * @og.rev 6.7.5.0 (2017/03/10) COLOR_CLM の値を、getValueLabel で取得するように変更。
    184         * @og.rev 6.9.7.0 (2018/05/14) imgタグに付与する、高さ、幅を、AbstractViewForm から、取得します。
    185         * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    186         *
    187         * @param  startNo        表示開始位置
    188         * @param  pageSize   表示件数
    189         *
    190         * @return      DBTableModelから作成された HTML文字列
    191         * @og.rtnNotNull
    192         */
    193        @Override
    194        public String create( final int startNo, final int pageSize )  {
    195                if( getRowCount() == 0 ) { return ""; } // 暫定処置
    196
    197                // パラメータの情報を初期化＆取得します。
    198                paramInit();
    199
    200                final int lastNo = getLastNo( startNo, pageSize );
    201
    202                // イメージの 最大Ｘ、Ｙサイズを求め、結果を、maxX,maxY 変数に設定する。
    203                calcImageSize( startNo,lastNo );
    204
    205                final BufferedImage img = new BufferedImage( maxX, maxY, BufferedImage.TYPE_INT_ARGB);
    206                final Graphics2D g2 = img.createGraphics();
    207
    208                // chartPadding を考慮した領域のクリップ（クリップ外にDrowされても無視されます。）
    209                g2.setClip( chartPadding , chartPadding , maxX-chartPadding*2+1 , maxY-chartPadding*2+1 );      // （x 座標,y 座標,幅,高さ) ＋１は罫線の分
    210
    211                final StringBuilder out = new StringBuilder( BUFFER_LARGE );
    212
    213                String oldKeyVal = "";                          // 初期値。１回目にキーブレイクさせる。
    214                long   oldTime   = 0L;                          // 日付項目の一つ前の値
    215
    216                Color  oldColor  = NULL_COLOR;          // 5.6.1.1 (2013/02/08) 不明(空欄)時の色(初期値)
    217
    218                // 6.0.2.1 (2014/09/26) fukurou.util.ColorMap とクラス名がかぶるので、こちらを変更します。
    219                final FlgColorMap colMap = new FlgColorMap();   // 状況コード、ラベル、カラーを管理するクラス
    220
    221                // 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    222                // ヘッダー文の考慮。rowCnt は、使わず、imgY は、加算していきます。
    223
    224                int imgY = chartPadding + (headerHeight+recodeMargin*2)*(useLegend ? 2 : 1);            // 6.4.6.1 (2016/06/03)
    225
    226                final int rowH = chartHeight+recodeMargin*2;    // 罫線出力時の高さ(drawRectで使用)
    227
    228                final double timeScale = (double)maxTimeWidth/(double)timeSpan;
    229                final boolean useTipsLink = tipsClmNo >= 0 || linkClmNo >= 0 ;          // tipsClm か linkClm かどちらかを使用する場合、true
    230
    231                for( int row=startNo; row<lastNo; row++ ) {
    232                        // キーブレイクの判定
    233                        final String keyVal  = getValue( row,KEY_CLMNO );
    234                        final String dyVal   = getValue( row,DY_CLMNO );
    235
    236                        // キーブレイク判定。キーブレイクは、一番初めから来る。
    237                        if( !oldKeyVal.equals( keyVal ) ) {
    238                                // キーブレイクで最初に行うのは、前のレコードの未出力分の処理。１行目は、処理不要
    239                                if( row > startNo ) {
    240                                        // 6.0.2.0 (2014/09/19) データが存在しない最後の処理で、現在時刻まで、継続しているとします。
    241                                        long lastTime = nowTime ;               // 現在時刻(分単位に変換する)
    242                                        // 5.9.10.5 (2016/07/08) 現在時刻がtimeSpanより大きいor最終値より小さい場合は、lastTimeをtimeSpanにする。
    243                                        if( lastTime > timeSpan || lastTime < oldTime ) { lastTime = timeSpan; }        // 5.9.10.5 (2016/07/08)
    244
    245                                        if( !useLastData ) { oldColor  = NULL_COLOR; }                  // 6.4.7.1 (2016/06/17)
    246                                        while( oldTime < timeSpan ) {
    247                                                // 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
    248                                                final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);               // イメージ出力時の X軸の左端 px数
    249                                                final int imgW = (int)((lastTime-oldTime)*timeScale) ;                                                  // イメージ出力時の 幅(fillRectで使用)
    250
    251                                                if( isDebug ) {
    252                                                        final int step = TimeScaleStep.getStep( timeScale );
    253                                                        final String stTM = getTime2Str(startTime+(int)oldTime ,step,0) ;
    254                                                        final String edTM = getTime2Str(startTime+(int)lastTime,step,0) ;
    255                                                        System.out.println( "② ROW[" + row + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
    256                                                }
    257                                                imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, row, out);      // 6.0.2.0 (2014/09/19)
    258
    259                                                oldTime   = lastTime ;
    260                                                lastTime  = timeSpan ;
    261                                                oldColor  = NULL_COLOR;         // 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
    262                                        }
    263                                        imgY += rowH ;                                  // 初期値にヘッダー高さを加算しているため、２回目以降に加算していく。 6.4.6.1 (2016/06/03)
    264                                }
    265
    266                                // 次に、新しい行の出力（ラベルは、ブレイク時に出力しておきます。）
    267                                oldKeyVal = keyVal;                     // null は来ないはず
    268                                // 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    269                                // ヘッダー文の考慮。rowCnt は、使わず、imgY は、加算していきます。
    270                                // レコードのラベル分だけ、繰返し描画する。
    271                                final int clmSu = labelClmsNo.length;
    272                                int posX  = chartPadding ;                                                              // ラベル文字列の書き出し位置の初期値
    273                                final int posY2 = imgY+chartHeight+recodeMargin ;               // ラベルのY軸基準。
    274                                g2.setColor( LABEL_COLOR );
    275                                for( int col=0; col<clmSu; col++ ) {
    276                                        final String lbl = StringUtil.spanCut( getValueLabel( row,labelClmsNo[col] ) );         // 5.6.3.1 (2013/04/05) spanタグを削除
    277                                        // 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    278//                                      g2.drawString( lbl , posX + recodeMargin, posY2 );                                                                      // 文字列,ベースラインのx座標,y座標
    279                                        if( useCR ) {
    280                                                final String[] xlbls = lbl.split( "\\n|\\\\n" );                                                                        // 本物の改行と、"\n" 文字列
    281                                                for( int i=0; i<xlbls.length; i++ ) {
    282                                                        final int posY3 = posY2 - (xlbls.length-1-i)*rowH/xlbls.length;                                 // 改行ラベルを、均等に枠内に分割します。
    283                                                        g2.drawString( xlbls[i] , posX + recodeMargin, posY3  );                                                // 文字列,ベースラインのx座標,y座標
    284                                                }
    285                                        }
    286                                        else {
    287                                                g2.drawString( lbl , posX + recodeMargin, posY2 );                                                                      // 文字列,ベースラインのx座標,y座標
    288                                        }
    289
    290                                        posX += recodeMargin*2 + maxClmWidth[col] ;                             // 次の書き出し位置は、文字最大長＋マージン
    291                                }
    292
    293                                // レコードの枠線
    294                                g2.drawRect( chartPadding , imgY , maxX-chartPadding*2 , rowH );                // (レコード枠)左端x,上端y,幅w,高さh
    295
    296                                oldTime = 0L;                           // キーブレイクによる初期化
    297                                oldColor= NULL_COLOR;           // 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
    298                        }
    299
    300                        long newTime = getStr2Date( dyVal ) - startDate;                // 次の時刻
    301                        if( newTime < oldTime ) { newTime = oldTime; }                  // 前の時刻より小さい場合は、前の時刻まで、進めておく。
    302
    303                        // 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
    304                        final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);               // old時刻から書き始める(左端x)
    305                        final int imgW = (int)((newTime-oldTime)*timeScale) ;                                                   // 差分幅だけ描画する。
    306
    307                        if( isDebug ) {
    308                                final int step = TimeScaleStep.getStep( timeScale );
    309                                final String stTM = getTime2Str(startTime+(int)oldTime,step,0) ;
    310                                final String edTM = getTime2Str(startTime+(int)newTime,step,0) ;
    311                                System.out.println( "① ROW[" + row + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
    312                        }
    313                        imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, row, out);      // 6.0.2.0 (2014/09/19)
    314
    315                        oldTime = newTime ;
    316
    317                        final String fgjVal  = getValue( row,FGJ_CLMNO );                       // 状況コードのキー
    318                        final String fgjLbl  = getValueLabel( row,FGJ_CLMNO );          // 状況コードのラベル
    319                        if( colClmNo >= 0 ) {
    320                                // 6.7.5.0 (2017/03/10) COLOR_CLM の値を、getValueLabel で取得するように変更。
    321                                oldColor  = colMap.getColor( fgjVal,fgjLbl,getValueLabel( row,colClmNo ) );     // 色文字列を指定する場合
    322                        }
    323                        else {
    324                                oldColor  = colMap.getColor( fgjVal,fgjLbl );                                                           // 色文字列を指定しない＝自動設定
    325                        }
    326                }
    327
    328                // レコードのいちばん最後の出力。一応、ジャストの場合(oldTime == maxEdTime)は、処理しない
    329                // 5.6.1.1 (2013/02/08) レコードのいちばん最後の出力は、NULL色を使うように変更
    330                long lastTime = nowTime ;               // 現在時刻(分単位に変換する)
    331                // 6.4.8.2 (2016/07/08) 未来日付の場合に、現在時刻まで巻き戻って塗りつぶしている。
    332                if( lastTime > timeSpan || lastTime < oldTime ) { lastTime = timeSpan; }                // 現在時刻が、timeSpanより大きい場合 OR 未来時刻の場合は、同じにする。
    333
    334                if( !useLastData ) { oldColor  = NULL_COLOR; }                  // 6.4.7.1 (2016/06/17)
    335                while( oldTime < timeSpan ) {
    336                        // 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
    337                        final int imgX = chartPadding + maxLabelWidth + (int)(oldTime*timeScale);               // イメージ出力時の X軸の左端 px数
    338                        final int imgW = (int)((lastTime-oldTime)*timeScale) ;                                                  // イメージ出力時の 幅(fillRectで使用)
    339
    340                        if( isDebug ) {
    341                                final int step = TimeScaleStep.getStep( timeScale );
    342                                final String stTM = getTime2Str(startTime+(int)oldTime ,step,0) ;
    343                                final String edTM = getTime2Str(startTime+(int)lastTime,step,0) ;
    344                                System.out.println( "③ ROW[" + lastNo + "]=" + stTM + " - " + edTM + " imgX=" + imgX + " imgW=" + imgW );
    345                        }
    346                        imageMeker(g2, oldColor, imgX, imgY, imgW, useTipsLink, lastNo, out);   // 6.0.2.0 (2014/09/19)
    347
    348                        oldTime   = lastTime ;
    349                        lastTime  = timeSpan ;
    350                        oldColor  = NULL_COLOR;         // 5.6.1.1 (2013/02/08) キーブレイクによる初期化=不明(空欄)時の色
    351                }
    352
    353                // ヘッダー情報のイメージを作成します。
    354                imageHeaderPaint( g2 , timeScale , colMap );
    355
    356                // イメージファイルを出力し、URLを返します。
    357                final File file ;                               // 6.3.9.0 (2015/11/06) Found 'DU'-anomaly for variable(PMD)
    358                try {
    359                        final File dir = new File( tempDir );           // 画像ファイル出力先のフォルダ
    360                        // 6.0.2.4 (2014/10/17) RV  java.io.File.mkdirs() の例外的戻り値を無視しています。
    361                        if( ! dir.exists() && ! dir.mkdirs() ) {
    362                                final String errMsg = "画像ファイル出力先のフォルダの作成に失敗しました。tempDir=[" + dir +"]" ;
    363                                throw new HybsSystemException( errMsg );
    364                        }
    365
    366                        file = File.createTempFile( "Img",".png", dir );        // テンポラリファイルの作成
    367                        file.deleteOnExit();                                                            // JavaVM 停止時にテンポラリファイルの削除
    368
    369                        ImageIO.write(img, "PNG", file );
    370                        g2.dispose();
    371                }
    372                catch( final IOException ex ) {
    373                        final String errMsg = "画像ファイルの作成に失敗しました。tempDir=[" + tempDir +"]" ;
    374                        throw new HybsSystemException( errMsg,ex );
    375                }
    376
    377                // 6.9.7.0 (2018/05/14) imgタグに付与する、高さ、幅を、AbstractViewForm から、取得します。
    378                // 高さ、幅は、一つでも指定されている場合は、AbstractViewForm の設定値を使用し、どちらも未設定の場合は、
    379                // 元の画像サイズ(maxX,maxY)を記述します。(imgタグのサイズ指定で、ブラウザの画像読み込み時の体感速度が向上します。)
    380                String width  = getWidth();                     // viewタグで設定した、幅
    381                String height = getHeight();            // viewタグで設定した、高さ
    382
    383                if( StringUtil.isEmpty( width ) && StringUtil.isEmpty( height ) ) {             // 両方とも、empty の場合のみ、ｵﾘｼﾞﾅﾙの値をセットします。
    384                        width  = maxX + "px" ;
    385                        height = maxY + "px" ;
    386                }
    387
    388                // imgタグを作成します。
    389                // 6.3.9.0 (2015/11/06) width,height にセットしているが、値を変更していないので、直接設定する。
    390                // 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
    391                return new OgBuilder()
    392                        .append( "<img id='ImageTimeBar' alt='ImageTimeBar' title='ImageTimeBar' border='0'" )
    393                        .appendNN( " width='"   , width                                         , "'" )                 // width が null の場合は、この行すべて登録しません。
    394                        .appendNN( " height='"  , height                                        , "'" )                 // heightが null の場合は、この行すべて登録しません。
    395                        .append(   " src='"             , tempUrl , file.getName()      , "'" )
    396//              return new OgBuilder()
    397//                      .append( "<img id='ImageTimeBar' alt='ImageTimeBar' title='ImageTimeBar' border='0'"
    398//                                      , " width='"            , String.valueOf( maxX )
    399//                                      , "px' height='"        , String.valueOf( maxY )
    400//                                      , "px' src='"           , tempUrl
    401//                                      , file.getName() , "'" )
    402                        // 6.4.4.2 (2016/04/01) 引数がSupplierに変更。
    403                        // area タグのデータが作成されていれば、出力します。
    404                        .appendCase( out.length() > 0
    405                                                        , () -> new String[] {  " usemap='#TimeBarMap' /><map name='TimeBarMap'>"
    406                                                                                                  , out.toString()
    407                                                                                                  , "</map>" }                          // true
    408                                                        , () -> new String[] {  " />" }                                 // false
    409                        ).toString();
    410        }
    411
    412        /**
    413         * データが存在しない最後の処理で、現在時刻まで、継続している処理をまとめます。
    414         *
    415         * このメソッドは、キーブレイク、通常の出力処理、データの最後の３か所に同じ処理が出てくるため
    416         * 一つに整理しました。
    417         *
    418         * @og.rev 6.0.2.0 (2014/09/19) 新規作成
    419         * @og.rev 6.4.2.0 (2016/01/29) alt属性にtitle属性を追記。
    420         * @og.rev 6.4.7.1 (2016/06/17) useLastData属性は、最後の処理のみに適用する。
    421         * @og.rev 6.4.8.2 (2016/07/08) クリッカブルマップの設定で、普通のリンクタグや、hrefOnly、hrefTarget が使えるようにする。
    422         * @og.rev 6.4.9.5 (2016/09/09) title属性を廃止したが、一旦戻します。(ネイティブモードでツールチップが表示しない)
    423         * @og.rev 6.7.9.1 (2017/05/19) 幅が0の場合は、1ピクセルだけ一つ前の領域に描画する。
    424         *
    425         * @param  g2                   描画オブジェクト(Graphics2D)
    426         * @param  oldColor             旧色
    427         * @param  imgX                 イメージ出力時の X軸の左端 px数
    428         * @param  imgY                 イメージ出力時の Y軸の上端 px数
    429         * @param  imgW                 イメージ出力時の 幅(fillRectで使用)
    430         * @param  useTipsLink  Tipsのリンクを作成するかどうか(true:作成する)
    431         * @param  row                  行番号
    432         * @param  outBuf               出力用 StringBuilder
    433         */
    434        private void imageMeker( final Graphics2D g2,final Color oldColor,
    435                                                         final int imgX,final int imgY,final int imgW,
    436                                                         final boolean useTipsLink,final int row,final StringBuilder outBuf ) {
    437                // 幅が0以上の場合のみ描画する。
    438                if( imgW > 0 ) {
    439                        // 6.4.7.1 (2016/06/17)
    440                        g2.setColor( oldColor );
    441                        g2.fillRect( imgX , imgY+recodeMargin , imgW, chartHeight );    // (実際の状態)左端x,上端y,幅w,高さh
    442
    443                        // 6.0.2.0 (2014/09/19) Tips表示を出さない条件に、oldColor == NULL_COLOR を加える。
    444                        // equals ではなく、!= でオブジェクトの直接比較で判定する。
    445                        // 6.4.7.1 (2016/06/17)
    446                        final boolean useTips = oldColor != NULL_COLOR && useTipsLink && row > 0 ;
    447
    448                        // tipsClm か linkClm を使用する。
    449                        // 5.6.1.0 (2013/02/01) tips や link は、ひとつ前のデータで作成する必要がる為、最初の1件目は、処理しないように修正
    450                        if( useTips ) {
    451                                // tips や link は、ひとつ前のデータで作成する必要がある。(row-1)
    452                                String tips = tipsClmNo >= 0 ? getValueLabel( row-1,tipsClmNo ) : getValueLabel( row-1,FGJ_CLMNO ) ;
    453                                if( tips != null && tips.indexOf( '\'' ) >= 0 ) {               // 5.6.3.0 (2013/04/01) シングルクォーテーション のエスケープ
    454                                        tips = tips.replaceAll( "'","&#39;" );
    455                                }
    456                                tips = StringUtil.spanCut( tips );                                              // 5.6.3.1 (2013/04/05) spanタグを削除
    457
    458                                outBuf.append( "<area shape='rect' alt='" ).append( tips ).append( "' title='" ).append( tips ).append( '\'' );                 // 6.4.9.5 (2016/09/09) 一旦戻す。
    459                                if( linkClmNo >= 0 ) {
    460                                        final String hrefVal = getValueLabel( row-1,linkClmNo );
    461                                        if( hrefVal != null ) {
    462                                                // 6.4.8.2 (2016/07/08) クリッカブルマップの設定で、普通のリンクタグや、hrefOnly、hrefTarget が使えるようにする。
    463                                                outBuf.append( ' ' );
    464                                                final int stAd = hrefVal.indexOf( "href" );
    465                                                final int edAd = hrefVal.indexOf( '>' );
    466                                                if( stAd >= 0 ) {                               // href が存在する。
    467                                                        if( edAd >= 0 ) { outBuf.append( hrefVal.substring( stAd,edAd ) ); }    // a href ･･･ の場合の処置
    468                                                        else            { outBuf.append( hrefVal.substring( stAd ) ); }                 // 従来の互換設定
    469                                                }
    470                                                else {
    471                                                        outBuf.append( " href='" ).append( hrefVal ).append( '\'' );                    // リンクタグの hrefOnlyや、hrefTarget が使えるようにする。
    472                                                }
    473                                        }
    474                                }
    475
    476                                // 6.4.8.2 (2016/07/08) title を、tips からキーの値に変更。ただし、未設定の場合のみ。
    477                                if( outBuf.indexOf( "title=" ) < 0 ) {
    478                                        final String title = getValueLabel( row-1,KEY_CLMNO );  // 6.4.8.2 (2016/07/08) title を、tips からキーの値に変更。
    479                                        outBuf.append( "' title='" ).append( title ).append( '\'' );
    480                                }
    481
    482                                // 6.0.2.5 (2014/10/31) char を append する。
    483                                outBuf.append( " coords='" ).append( imgX )
    484                                        .append( ',' ).append( imgY+recodeMargin )                              // 左端x1,上端y1
    485                                        .append( ',' ).append( imgX+imgW )
    486                                        .append( ',' ).append( imgY+recodeMargin+chartHeight )  // 右端x2,下段y2
    487                                        .append( "' />\n" );
    488                        }
    489                }
    490                //  6.7.9.1 (2017/05/19) 幅が0の場合は、1ピクセルだけ一つ前の領域に描画する。
    491                else {
    492                        g2.setColor( oldColor );
    493                        g2.fillRect( imgX -1 , imgY+recodeMargin , 1, chartHeight );    // (実際の状態)左端x,上端y,幅w,高さh
    494                }
    495        }
    496
    497        /**
    498         * パラメータ内容を初期化します。
    499         *
    500         * @og.rev 5.6.1.1 (2013/02/08) useLastData 追加
    501         * @og.rev 6.0.2.0 (2014/09/19) nowTime データがない最後の処理で使用する現在時刻
    502         * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    503         * @og.rev 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします。
    504         */
    505        private void paramInit() {
    506                final String s_startDate        = getParam( "START_DATE"                );      // タイムテーブルの表示開始日時をセットします(初期値:データの最小日時)。
    507                final int timeSpanHour          = getIntParam( "TIME_SPAN"              );      // タイムテーブルの表示期間を時間で指定します(初期値:{@og.value TIME_SPAN})。
    508
    509                final String[] s_labelClms      = StringUtil.csv2Array( getParam( "LABEL_CLMS" ) );     // 一覧表のラベル表示部に表示するカラム名をCSV形式で指定します。
    510                final String s_colClm           = getParam( "COLOR_CLM"         );      // レコードに付ける色を色文字列で指定する場合のカラム名を指定します。
    511                final String s_tipsClm          = getParam( "TIPS_CLM"          );      // レコード単位に、マウスオーバー時のTips表示を行うカラム名を指定します。
    512                final String s_linkClm          = getParam( "LINK_CLM"          );      // レコード単位に、クリッカブルリンクを設定するカラム名を指定します。
    513
    514                useLegend               = getBoolParam( "USE_LEGEND"            );      // カラーの凡例を使用するかどうか[true/false]
    515                maxLabelWidth   = getIntParam( "MAX_LABEL_WIDTH"        );      // ラベル表記部の最大サイズをpxで指定。何もなければ、可変長サイズ
    516                maxTimeWidth    = getIntParam( "MAX_TIME_WIDTH"         );      // タイム表記部の最大サイズをpxで指定。
    517                chartHeight             = getIntParam( "CHART_HEIGHT"           );      // １レコードのチャートの間隔をpxで指定。実際の幅は、CHART_HEIGHT＋MARGIN＊２
    518                headerHeight    = getIntParam( "HEADER_HEIGHT"          );      // 6.4.6.1 (2016/06/03) ヘッダー部の高さをpxで指定。凡例とヘッダーの高さになります。
    519                if( headerHeight < 0 ) { headerHeight = chartHeight; }  // 指定が無い場合は、チャート幅と同じにします。
    520                chartPadding    = getIntParam( "CHART_PADDING"          );      // イメージ作成の 全体テーブルの隙間
    521                recodeMargin    = getIntParam( "RECODE_MARGIN"          );      // 各レコード、文字等の内部の間隔
    522                useLastData             = getBoolParam( "USE_LAST_DATA"         );      // 5.6.1.1 (2013/02/08) 行の最後の情報が、継続しているとして使うかどうか[true/false]
    523                isDebug                 = getBoolParam( "debug"                         );      // 6.4.8.2 (2016/07/08) debug 属性を、パラメータにセットします[true/false]。
    524
    525                tempDir                 = getParam( "TEMP_DIR"                          );      // 画像ファイルを作成するテンポラリディレクトリ(相対パス)
    526                tempUrl                 = getParam( "TEMP_URL"                          );      // 作成した画像ファイルを取得するときに使用するURL(コンテキスト/相対パス)
    527
    528                startDate               = getStr2Date( s_startDate );                   // 分単位に変換する。
    529                startTime               = str2DateTime ;                                                // 開始日時の時刻情報(分単位)の値。str2DateTime は、getStr2Date(String)メソッド実行後にセットされる。
    530                timeSpan                = timeSpanHour * 60L ;                                  // 分単位に変換する。
    531
    532                final int len = s_labelClms.length;
    533                if( len > 0 ) {
    534                        labelClmsNo = new int[len];
    535                        for( int i=0; i<len; i++ ) {
    536                                labelClmsNo[i] = getColumnNo( s_labelClms[i] );
    537                        }
    538                }
    539                else {
    540                        labelClmsNo = new int[] { KEY_CLMNO };          // 初期値は、キーカラム
    541                }
    542
    543                // 指定のカラム名に対するカラム番号を取得。なければ、-1 を設定しておく。
    544                if( s_colClm  != null ) { colClmNo  = getColumnNo( s_colClm  ); }                       // レコードに付ける色を色文字列で指定する場合のカラムNo
    545                if( s_tipsClm != null ) { tipsClmNo = getColumnNo( s_tipsClm ); }                       // レコード単位に、マウスオーバー時のTips表示を行うカラムNo
    546                if( s_linkClm != null ) { linkClmNo = getColumnNo( s_linkClm ); }                       // レコード単位に、クリッカブルリンクを設定するカラムNo
    547
    548                // 6.0.2.0 (2014/09/19) データがない最後の処理で使用する現在時刻
    549                final Calendar cal = Calendar.getInstance();
    550                nowTime = cal.getTimeInMillis() / MILLI_MINUTE - startDate ;            // 6.0.2.0 (2014/09/19) ミリ秒を分に換算、現在時刻(分単位)
    551        }
    552
    553        /**
    554         * イメージの ＸＹサイズを求め、結果を、maxX,maxY 変数に設定します。
    555         *
    556         * また、ラベル文字列の最大長が指定されていない場合（maxLabelWidth == -1）最大長を自動計算し、maxLabelWidth変数にセットします。
    557         *
    558         * maxLabelWidth : -1 の場合は、ラベル文字列の最大長を求めて、この値を計算する。= (recodeMargin*2 + ラベル文字列の最大長)
    559         * maxX : イメージの最大横幅(X)方向のサイズpx。chartPadding*2 + maxLabelWidth + maxTimeWidth
    560         * maxY : イメージの最大縦幅(Y)方向のサイズpx。chartPadding*2 + (chartHeight+recodeMargin*2)*(レコード数＋ヘッダー数)
    561         *
    562         * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、<span>タグが付与される値から、spanタグを削除します。
    563         * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    564         * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    565         *
    566         * @param       startNo 計算の開始列番号
    567         * @param       lastNo  計算の終了列番号
    568         */
    569        private void calcImageSize( final int startNo , final int lastNo ) {
    570                String oldKeyVal = "";  // 初期値。１回目にキーブレイクさせる。
    571
    572                final int clmSu = labelClmsNo.length;
    573                maxClmWidth = new int[clmSu];
    574
    575                // 6.4.6.1 (2016/06/03) ヘッダー部の高さは、凡例とヘッダー部が対象。
    576                int rowCnt = 0;                                         // 6.4.6.1 (2016/06/03) レコード数
    577
    578                // ラベル領域の長さを各ラベル長を積み上げて計算する。
    579                if( maxLabelWidth < 0 ) {
    580                        // FontMetrics を取得する為だけの BufferedImage
    581                        final BufferedImage img = new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB);
    582                        final Graphics2D g2 = img.createGraphics();
    583                        final FontMetrics fontM = g2.getFontMetrics();
    584
    585                        // 初期値の計算は、ヘッダーのラベルの幅を使用する。
    586                        for( int col=0; col<clmSu; col++ ) {
    587                                final String lbl = StringUtil.spanCut( getColumnLabel( labelClmsNo[col] ) );            // 5.6.3.1 (2013/04/05) spanタグを削除
    588//                              maxClmWidth[col] = fontM.stringWidth( lbl );                                                                            // 最大値の初期値として、ヘッダーラベルの幅をセットしておく。
    589                                // 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    590                                final String[] xlbls = lbl.split( "\\n|\\\\n" );                                                                        // 本物の改行と、"\n" 文字列
    591                                if( xlbls.length > 1 ) {
    592                                        useCR = true;
    593                                        for( final String xlbl : xlbls ) {
    594                                                final int fontW = fontM.stringWidth( xlbl );
    595                                                if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }                            // 分割したラベルで、最大値を求める。
    596                                        }
    597                                }
    598                                else {
    599                                        maxClmWidth[col] = fontM.stringWidth( lbl );                                                                    // 最大値の初期値として、ヘッダーラベルの幅をセットしておく。
    600                                }
    601                        }
    602
    603                        for( int row=startNo; row<lastNo; row++ ) {
    604                                // キーブレイク判定。キーブレイクは、一番初めから来る。
    605                                final String keyVal = getValue( row,KEY_CLMNO );
    606                                if( !oldKeyVal.equals( keyVal ) ) {
    607                                        oldKeyVal = keyVal;
    608                                        rowCnt++;                               // レコード数
    609
    610                                        // ラベルは、キーブレイク時の値のみ採用する。
    611                                        for( int col=0; col<clmSu; col++ ) {
    612                                                final String lbl = StringUtil.spanCut( getValueLabel( row,labelClmsNo[col] ) );         // 5.6.3.1 (2013/04/05) spanタグを削除
    613//                                              final int fontW = fontM.stringWidth( lbl );
    614//                                              if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }
    615
    616                                                // 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    617                                                final String[] xlbls = lbl.split( "\\n|\\\\n" );                                                                        // 本物の改行と、"\n" 文字列
    618                                                if( xlbls.length > 1 ) {
    619                                                        useCR = true;
    620                                                        for( final String xlbl : xlbls ) {
    621                                                                final int fontW = fontM.stringWidth( xlbl );
    622                                                                if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }                                    // 分割したラベルで、最大値を求める。
    623                                                        }
    624                                                }
    625                                                else {
    626                                                        final int fontW = fontM.stringWidth( lbl );
    627                                                        if( maxClmWidth[col] < fontW ) { maxClmWidth[col] = fontW; }
    628                                                }
    629                                        }
    630                                }
    631                        }
    632                        g2.dispose();
    633
    634                        // 最大ラベル幅は、各ラベルの最大値＋（マージン＊２）＊カラム数
    635                        maxLabelWidth = recodeMargin * 2 * clmSu ;
    636                        for( int col=0; col<clmSu; col++ ) {
    637                                maxLabelWidth += maxClmWidth[col];
    638                        }
    639                }
    640                else  {
    641                        for( int row=startNo; row<lastNo; row++ ) {
    642                                // キーブレイク判定。キーブレイクは、一番初めから来る。
    643                                final String keyVal = getValue( row,KEY_CLMNO );
    644                                if( !oldKeyVal.equals( keyVal ) ) {
    645                                        oldKeyVal = keyVal;
    646                                        rowCnt++;                               // レコード数
    647                                }
    648                        }
    649
    650                        // 最大ラベル幅は、均等割り付け。端数は無視（どうせ、ラベル部は、maxLabelWidth で計算するので。）
    651                        final int clmWidth = ( maxLabelWidth - recodeMargin * 2 * clmSu ) / clmSu ;
    652                        for( int col=0; col<clmSu; col++ ) {
    653                                maxClmWidth[col] = clmWidth;
    654                        }
    655                }
    656
    657                maxX = chartPadding*2 + maxLabelWidth + maxTimeWidth ;
    658                // 6.4.6.1 (2016/06/03) ヘッダー部の高さは、凡例とヘッダー部を考慮して、加算する。
    659                maxY = chartPadding*2 + (headerHeight+recodeMargin*2)*(useLegend ? 2 : 1) + (chartHeight+recodeMargin*2)*rowCnt ;
    660        }
    661
    662        /**
    663         * ヘッダー情報のイメージを作成します。
    664         *
    665         * 全体の枠もここで作成しておきます。
    666         * イメージは、キーカラムのラベルと、時間軸になります。時間軸は縦方向にすべて線を引きます。
    667         * 時間軸の間隔は、timeScale によって、切り替わります。
    668         * 凡例を使う場合（useLegend=true）は、引数の FlgColorMap を利用して作成します。
    669         *
    670         * @og.rev 5.6.3.1 (2013/04/05) 短縮ラベルなど、<span>タグが付与される値から、spanタグを削除します。
    671         * @og.rev 5.6.5.0 (2013/06/07) 年月日情報を表示します。なお、日単位の場合は、年月は省略します。
    672         * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
    673         * @og.rev 6.4.6.1 (2016/06/03) ヘッダー部の高さを指定できるようにする。
    674         * @og.rev 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    675         *
    676         * @param       g2                      描画するGraphics2Dオブジェクト
    677         * @param       timeScale       時間(分)当たりのピクセル数
    678         * @param       colMap          状況コードに対応したカラーマップ
    679         */
    680        private void imageHeaderPaint( final Graphics2D g2 , final double timeScale , final FlgColorMap colMap ) {
    681
    682                int posY1 = chartPadding  ;
    683                int posY2 = chartPadding+headerHeight+recodeMargin ;            // 6.4.6.1 (2016/06/03) ヘッダー部の高さ
    684
    685                // 凡例を使う場合
    686                if( useLegend && colMap != null ) {
    687                        // 凡例を並べる間隔を求める。
    688                        final FontMetrics fontM = g2.getFontMetrics();
    689                        final int maxSize = fontM.stringWidth( colMap.getMaxLengthLabel() ) ;           // 文字列の最大長ラベルの幅
    690                        final int imgW  = headerHeight ;                        // 凡例■の幅(高さと同じにしているので真四角) 、6.4.6.1 (2016/06/03)
    691                        final int mgnW  = recodeMargin ;                        // 凡例■から文字までの間
    692                        final int spanW = maxSize + recodeMargin ;      // 凡例■から凡例■までの間隔。文字最大長＋α
    693
    694                        int legX  = chartPadding ;
    695
    696                        // 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
    697                        for( final FlgColorObj obj : colMap.values() ) {
    698                                g2.setColor( obj.COL );                                                                                 // 凡例■の色
    699                                g2.fillRect( legX , posY1+recodeMargin , imgW , headerHeight ); // (実際の状態)左端x,上端y,幅w,高さh、6.4.6.1 (2016/06/03)
    700
    701                                legX += imgW + mgnW ;
    702                                g2.setColor( LABEL_COLOR );
    703                                g2.drawString( obj.LBL , legX , posY2 );        // 文字列,ベースラインのx座標,y座標
    704
    705                                legX += spanW ;
    706                        }
    707                        posY1 += headerHeight+recodeMargin*2 ;  // １レコード分の高さを加算しておく。、6.4.6.1 (2016/06/03)
    708                        posY2 += headerHeight+recodeMargin*2 ;  // １レコード分の高さを加算しておく。、6.4.6.1 (2016/06/03)
    709                }
    710
    711                // まずは、全体の枠線の描画
    712                g2.setColor( LABEL_COLOR );
    713                g2.drawRect( chartPadding, posY1, maxX-chartPadding*2, maxY-posY1-chartPadding );                               // 左端,上端,幅,高さ
    714
    715                // ヘッダーのラベル分だけ、繰返し描画する。
    716                final int clmSu = labelClmsNo.length;
    717                int posX  = chartPadding ;              // ラベル文字列の書き出し位置の初期値
    718                for( int col=0; col<clmSu; col++ ) {
    719                        // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
    720                        final String lbl = StringUtil.spanCut( getColumnLabel( labelClmsNo[col] ) );    // 5.6.3.1 (2013/04/05) spanタグを削除
    721//                      g2.drawString( lbl , posX + recodeMargin, posY2 );                      // 文字列,ベースラインのx座標,y座標
    722                        // 6.9.9.1 (2018/08/27) ラベル領域の長さ求めで、改行が含まれる場合の計算対応。
    723                        if( useCR ) {
    724                                final String[] xlbls = lbl.split( "\\n|\\\\n" );                                                                        // 本物の改行と、"\n" 文字列
    725                                for( int i=0; i<xlbls.length; i++ ) {
    726                                        final int posY3 = posY2 - (xlbls.length-1-i)*headerHeight/xlbls.length;                 // 改行ラベルを、均等に枠内に分割します。
    727                                        g2.drawString( xlbls[i] , posX + recodeMargin, posY3  );                                                // 文字列,ベースラインのx座標,y座標
    728                                }
    729                        }
    730                        else {
    731                                g2.drawString( lbl , posX + recodeMargin, posY2 );                      // 文字列,ベースラインのx座標,y座標
    732                        }
    733
    734                        posX += recodeMargin*2 + maxClmWidth[col] ;                                     // 次の書き出し位置は、文字最大長＋マージン*2
    735                        g2.drawLine( posX, posY1, posX, maxY-chartPadding );            // 始点(x 座標,y 座標),終点(x 座標,y 座標)
    736                }
    737
    738                final int step = TimeScaleStep.getStep( timeScale );                    // 時間スケールに対応したSTEP数
    739
    740                // 日付ヘッダー部の描画のためのカレンダー
    741                final Calendar cal = Calendar.getInstance();
    742                cal.setTimeInMillis( startDate * MILLI_MINUTE );                                // 6.0.2.0 (2014/09/19) ミリ秒に換算した分
    743
    744                final int week = cal.get( Calendar.DAY_OF_WEEK );       // 開始曜日
    745
    746                final String fmt = step < MINUTE_OF_DAY ? "yyyy/MM/dd(EE)" : "dd" ;             // 上段フォーマットは、時間ベースの場合は、"yyyy/MM/dd(EE)" 、日ベースの場合は、"dd"
    747                final DateFormat format1 = new SimpleDateFormat( fmt,Locale.JAPAN );
    748
    749                // グラフ領域の日付ヘッダー部の描画
    750                g2.setStroke( DSAH_STROK );                             // 日付部は、破線
    751                posX = chartPadding+maxLabelWidth ;             // グラフ領域は、chartPadding+maxLabelWidth から。
    752                for( int tm=0; tm<timeSpan; tm+=step ) {
    753
    754                        int offset = headerHeight / 2 + recodeMargin;   // ヘッダーの表示基準のオフセット（チャート幅の半分）6.4.6.1 (2016/06/03)
    755
    756                        // 上段：ヘッダー出力
    757                        if( tm % MINUTE_OF_DAY == 0 ) {                 // 6.0.2.0 (2014/09/19) １日が何分
    758                                final Date dt = new Date( (startDate + tm) * MILLI_MINUTE );                                            // 6.0.2.0 (2014/09/19) ミリ秒に換算した分
    759                                g2.drawString( format1.format( dt ) , posX + recodeMargin , posY2-offset );                     // 文字列,ベースラインのx座標,y座標
    760                                offset = 0;             // ヘッダーを表示する場合のみ上まで線を引く。
    761                        }
    762
    763                        g2.drawString( getTime2Str(startTime+tm,step,week) , posX + recodeMargin , posY2 );             // 文字列,ベースラインのx座標,y座標
    764                        g2.drawLine( posX, posY1+offset, posX, maxY-chartPadding );                                                             // 始点(x 座標,y 座標),終点(x 座標,y 座標)
    765
    766                        posX += (int)(step*timeScale);
    767                }
    768        }
    769
    770        /**
    771         * 時間スケールに対応したSTEP数を管理するための内部クラス
    772         *
    773         * 時間ヘッダーを表示する場合、ある程度意味のある間隔でラベル表示したいと思います。
    774         * 全体の描画領域の長さと、時間当たりのスケールファクター（ピクセル数）から、
    775         * ラベルの描画間隔を求めます。
    776         * 意味のある間隔は、STEPS で定義し、10分,30分,60分,1/4日,1/2日,1日 まで定義しています。
    777         * 一番大きな単位以上の場合は、その最大単位の整数倍を返します。
    778         *
    779         * 一時間当たりの表示幅を、MIN_PX としていますので、この値以下の間隔では描画されません。
    780         * 初期値は、600px を 24時間表示できる 600px/24h = 25px にしています。
    781         */
    782        private static final class TimeScaleStep {
    783                                                                                                        // 分   分   時   1/4   1/2  １日
    784                private static final int[] STEPS = new int[] { 10 , 30 , 60 , 360 , 720 , 1440 };
    785                private static final int MIN_PX = 25;           // スケールに対する最小値
    786
    787                /**
    788                 * オブジェクトを作らせない為の、private コンストラクタ
    789                 */
    790                private TimeScaleStep() {}
    791
    792                /**
    793                 * 時間を意味のある範囲の整数として返します。
    794                 *
    795                 * 全体の描画領域の長さと、時間当たりのスケールファクター（ピクセル数）から、
    796                 * 10分,30分,60分,1/4日,1/2日,1日 までの整数値で返します。
    797                 * 一番大きな単位以上の場合は、その最大単位の整数倍を返します。
    798                 *
    799                 * @param       timeScale       時間(分)当たりのピクセル数
    800                 * @return      時間スケールに対応した意味のある範囲の整数
    801                 */
    802                public static int getStep( final double timeScale ) {
    803                        final int tmStep = (int)Math.ceil(MIN_PX/(timeScale));  // 整数切り上げ
    804
    805                        for( int i=0; i<STEPS.length; i++ ) {
    806                                if( tmStep <= STEPS[i] ) { return STEPS[i]; }   // 配列の数字に切り上げ
    807                        }
    808
    809                        // 未設定の場合は、最上位の値の整数倍に切り上げ
    810                        return (int)Math.ceil( (double)tmStep / STEPS[STEPS.length-1] ) * STEPS[STEPS.length-1];
    811                }
    812        }
    813
    814        /**
    815         * 状況コード、ラベル、色を管理するための内部クラス
    816         *
    817         * 状況に応じたコード、ラベル、色を管理します。
    818         * これは、getColor(状況コード,ラベル) または、getColor(状況コード,ラベル,色文字列) で
    819         * 要求された情報を内部で管理し、同じコードの場合に同じ色を返します。
    820         * また、凡例作成用に、最も文字数が長いラベルを管理します。
    821         * 色文字列を指定した場合でも、最初に要求された状況コードに対応する色を返します。
    822         * これは、同一状況コードで色違いを作成することができないことを意味します。
    823         * 色文字列を指定しない場合は、内部の色配列から、順番に色を割り当てます。
    824         * 色を割り当てる順番は、状況コードの発生順です。よって、検索条件によって、
    825         * 状況コードの現れる順番が異なると、色も毎回異なることになります。
    826         *
    827         * 自動割り当ての色は、BLUE,CYAN,GRAY,GREEN,LIGHT_GRAY,MAGENTA,DARK_GRAY,ORANGE,PINK,RED,YELLOW
    828         * となっており、それを超えると、RGBをランダムに発生させて色を作成します。
    829         * よって、どのような色になるかは全くわかりません。
    830         *
    831         * @og.rev 6.4.3.3 (2016/03/04) CLR_ARY等、カラー関連処理を、ColorMapクラスへ移動します。
    832         */
    833        private static final class FlgColorMap {
    834                // 6.4.3.3 (2016/03/04) ColorMapクラスへ移動
    835                private final Map<String,FlgColorObj> colMap = new TreeMap<>();                                                                         // 6.3.9.0 (2015/11/06)
    836
    837                private int             lastCnt         ;
    838                private String  maxLabel        = "" ;          // 最大長のラベル
    839                private int             maxlen          = -1 ;          // 最大長のラベルのlength()
    840
    841                /**
    842                 * 状況コードに対応した色オブジェクトを返します。
    843                 *
    844                 * 状況コードが初めて指定された場合は、順番に内部の色を割り当てます。
    845                 * また、その時のラベルも管理します。ラベルと色のセットは、凡例作成時に利用されます。
    846                 *
    847                 * 自動割り当ての色は、BLUE,CYAN,GRAY,GREEN,LIGHT_GRAY,MAGENTA,DARK_GRAY,ORANGE,PINK,RED,YELLOW
    848                 * となっており、それを超えると、RGBをランダムに発生させて色を作成します。
    849                 * よって、どのような色になるかは全くわかりません。
    850                 *
    851                 * @param       fgj     状況コード
    852                 * @param       lbl     状況コードのラベル
    853                 * @return      状況コードに対応した色オブジェクト
    854                 */
    855                public Color getColor( final String fgj,final String lbl ) {
    856                        return getColor( fgj,lbl,null );
    857                }
    858
    859                /**
    860                 * 状況コードに対応した色オブジェクトを返します。
    861                 *
    862                 * 状況コードが初めて指定された場合は、引数の色文字列の色を割り当てます。
    863                 * また、その時のラベルも管理します。ラベルと色のセットは、凡例作成時に利用されます。
    864                 *
    865                 * 色文字列を指定した場合でも、最初に要求された状況コードに対応する色を返します。
    866                 * これは、同一状況コードで色違いを作成することができないことを意味します。
    867                 * 色文字列 が null の場合は、自動割り当てのメソッドと同じです。
    868                 * よって、色文字列の指定と、自動割り当てが同時に発生すると、異なる状況コードで
    869                 * 同じ色が指定される可能性がありますので、混在して使用しないでください。
    870                 *
    871                 * @og.rev 6.0.2.1 (2014/09/26) StringUtil → ColorMap
    872                 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
    873                 * @og.rev 6.4.3.3 (2016/03/04) ColorMapクラスへ移動。それに関連する修正。
    874                 *
    875                 * @param       fgj             状況コード
    876                 * @param       lbl             状況コードのラベル
    877                 * @param       colStr  状況コードに対応した色文字列(nullの場合は、自動割り当て)
    878                 * @return      状況コードに対応した色オブジェクト
    879                 * @og.rtnNotNull
    880                 */
    881                public Color getColor( final String fgj,final String lbl,final String colStr ) {
    882                        // 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)
    883                        final Color rtn ;
    884                        if( fgj == null ) { rtn = LABEL_COLOR; }
    885                        else {
    886                                if( lbl != null ) {
    887                                        final int len = lbl.length();
    888                                        if( len > maxlen ) { maxLabel = lbl; maxlen = len; }
    889                                }
    890
    891                                // Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
    892                                // colMap からは、FlgColorObj が戻されるので、COL 属性を取得する。
    893                                final String cloCnt = colStr == null ? String.valueOf( lastCnt++ ) : colStr ;
    894                                rtn = colMap.computeIfAbsent( fgj , k -> new FlgColorObj( lbl , cloCnt ) ).COL;
    895                        }
    896
    897                        return rtn ;
    898                }
    899
    900                /**
    901                 * 内部で管理している、ラベル(String)と色オブジェクト(Color)の コレクションを返します。
    902                 *
    903                 * 内部で管理しているコレクションです。
    904                 * このコレクションは、状況コードでソートされています。
    905                 * コレクションの中身は、オブジェクト配列となっており、[0]は、String型のラベル、[1]は、
    906                 * Color型の色です。
    907                 *
    908                 * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
    909                 *
    910                 * @return      ラベル(String)と色オブジェクト(Color)の コレクション
    911                 */
    912                public Collection<FlgColorObj> values() {
    913                        return colMap.values();
    914                }
    915
    916                /**
    917                 * 登録されたラベル文字列で、最も文字数が長いラベルを返します。
    918                 *
    919                 * 凡例で、ラベルの最大長を求めるのに利用できます。
    920                 * ただし、簡易的に、length() 計算しているだけなので、英語、日本語混在や、
    921                 * プロポーショナルフォント使用時の厳密な最大長の文字列ではありません。
    922                 *
    923                 * @return      最も文字数が長いラベル
    924                 */
    925                public String getMaxLengthLabel() { return maxLabel; }
    926        }
    927
    928        /**
    929         * FlgColorMapで管理している内部クラス
    930         *
    931         * フラグに対して、Object[] 配列の [0]:ラベル、[1]:カラー で管理してましたが、
    932         * きちんとString と Color で管理します。そのための内部クラスです。
    933         *
    934         * @og.rev 6.3.9.0 (2015/11/06) FlgColorMapで管理している内部クラスを新規作成します。
    935         */
    936        private static final class FlgColorObj {
    937                private final String LBL ;
    938                private final Color  COL ;
    939
    940                /**
    941                 * ラベルとカラーを指定したインストラクター
    942                 *
    943                 * @param       lbl     ラベル
    944                 * @param       col     カラー
    945                 */
    946                FlgColorObj( final String lbl , final Color col ) {
    947                        LBL = lbl ;
    948                        COL = col ;
    949                }
    950
    951                /**
    952                 * ラベルとカラー文字列を指定したインストラクター
    953                 *
    954                 * カラー文字列は、ColorMap で定義されている文字列です。
    955                 *
    956                 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
    957                 *
    958                 * @param       lbl             ラベル
    959                 * @param       colStr  カラー文字列
    960                 */
    961                FlgColorObj( final String lbl , final String colStr ) {
    962                        LBL = lbl ;
    963                        COL = ColorMap.getColorInstance( colStr );
    964                }
    965
    966                /**
    967                 * ラベルとカラー番号を指定したインストラクター
    968                 *
    969                 * カラー番号は、ColorMap で定義されている番号です。
    970                 *
    971                 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
    972                 *
    973                 * @param       lbl     ラベル
    974                 * @param       no      カラー番号
    975                 */
    976                FlgColorObj( final String lbl , final int no ) {
    977                        LBL = lbl ;
    978                        COL = ColorMap.getColorInstance( no );
    979                }
    980
    981                /**
    982                 * ラベルを返します。
    983                 *
    984                 * @og.rev 6.4.3.3 (2016/03/04) 新規追加。
    985                 *
    986                 * @return      ラベル
    987                 */
    988                /* default */ String getLabel() { return LBL; }
    989
    990                /**
    991                 * カラーを返します。
    992                 *
    993                 * @return      カラー
    994                 */
    995                /* default */ Color  getColor() { return COL; }
    996        }
    997
    998        /**
    999         * 日時文字列を数字に変換します。
    1000         *
    1001         * 日時文字列は、yyyyMMdd または、yyyyMMddhhmmss 形式とします。
    1002         * これを、エポックタイムからの経過時間の 分単位の値を求めます。
    1003         * 具体的には、Calendar オブジェクトの getTimeInMillis() の結果を、
    1004         * 60000 で割り算した値を作成します。
    1005         * よって、Calendar オブジェクト作成時も、秒の単位は切り捨てます。
    1006         * 引数が null の場合は、現在時刻より、作成します。
    1007         *
    1008         * @param       val     日時文字列の値(yyyyMMdd または、yyyyMMddhhmmss 形式 など)
    1009         *
    1010         * @return      日時文字列を分換算した数字
    1011         */
    1012        private long getStr2Date( final String val ) {
    1013                final Calendar cal = Calendar.getInstance();
    1014                str2DateTime = 0;
    1015                if( val == null ) {
    1016                        cal.set( Calendar.HOUR_OF_DAY, 0 );             // 5.3.5.0 (2011/05/01) 時間の解決規則が適用されるため、｢時｣だけは、setメソッドで 0 にセットする。
    1017                        cal.clear( Calendar.MINUTE );
    1018                        cal.clear( Calendar.SECOND );
    1019                        cal.clear( Calendar.MILLISECOND );
    1020                }
    1021                else if( val.length() == 8 ) {
    1022                        cal.clear();
    1023                        cal.set( Integer.parseInt( val.substring( 0,4 ) ) ,                     // 年
    1024                                         Integer.parseInt( val.substring( 4,6 ) ) - 1,          // 月(0から始まる)
    1025                                         Integer.parseInt( val.substring( 6,8 ) )                       // 日
    1026                        );
    1027                }
    1028                else {
    1029                        cal.clear();
    1030                        cal.set( Integer.parseInt( val.substring( 0,4 ) ) ,                     // 年
    1031                                         Integer.parseInt( val.substring( 4,6 ) ) - 1,          // 月(0から始まる)
    1032                                         Integer.parseInt( val.substring( 6,8 ) ) ,                     // 日
    1033                                         Integer.parseInt( val.substring( 8,10 ) ) ,            // 時
    1034                                         Integer.parseInt( val.substring( 10,12 ) )             // 分
    1035                        );
    1036                        str2DateTime = Integer.parseInt( val.substring( 8,10 ) ) * 60 + Integer.parseInt( val.substring( 10,12 ) ) ;
    1037                }
    1038                return cal.getTimeInMillis() / MILLI_MINUTE ;           // 6.0.2.0 (2014/09/19) ミリ秒に換算した分
    1039        }
    1040
    1041        /**
    1042         * 数字(分)を時間文字列に変換します。
    1043         *
    1044         * 480 は、"08" に、1260 は、"21" に変換します。
    1045         * 引数の時間は、分を表す整数です。２４時間表記であれば、0 ～ 1440 の範囲で収まりますが、
    1046         * 期間が長い場合は、その値を超えます。また、２４時間を超える場合は、0 に戻ります。
    1047         * 文字列にする場合の最小単位は、(時)なので、60(分)で割り算して、余は、切り捨てます。
    1048         * step は、60(分)単位の表示時に飛ばす数です。step=60 なら、60(分)単位、step=120 なら、120(分)
    1049         * 単位となります。stepが、1440 以下の場合は、そのまま、24時間表記で構いませんが、
    1050         * それを超えると時間ではなく、日数表記に切り替わります。
    1051         *
    1052         * @og.rev 5.6.5.0 (2013/06/07) 月単位の場合は、曜日を表示します。
    1053         *
    1054         * @param       timeVal 引数の時間整数(分)
    1055         * @param       step    60分単位のステップ数( 10,30,60,720,1440 単位となるように調整)
    1056         * @param       week    カレンダクラスの曜日フィールド(DAY_OF_WEEK)
    1057         *
    1058         * @return      数字を時間文字列に変換した結果( "08" など)
    1059         * @og.rtnNotNull
    1060         */
    1061        private String getTime2Str( final int timeVal, final int step, final int week ) {
    1062
    1063                final StringBuilder rtn = new StringBuilder( BUFFER_MIDDLE );
    1064
    1065                if( step >= MINUTE_OF_DAY ) {                                                           // 6.0.2.0 (2014/09/19) １日が何分
    1066                        final int dtm = timeVal / MINUTE_OF_DAY ;                               // 日の整数値
    1067                        rtn.append( DAY_OF_WEEK_JA[ ( dtm + week ) % W_7 ] );   // 曜日を表示
    1068                }
    1069                else {
    1070                        final int htm = (timeVal / M_60) % H_24 ;               // 時(２４時間制)
    1071                        if( htm < DEC ) { rtn.append( '0' ); }                  // 6.0.2.5 (2014/10/31) char を append する。
    1072                        rtn.append( htm );
    1073
    1074                        if( step < M_60 ) {
    1075                                final int mtm = timeVal % M_60 ;                        // 分(６０分制)
    1076                                rtn.append( ':' );                                                      // 6.0.2.5 (2014/10/31) char を append する。
    1077                                if( mtm < DEC ) { rtn.append( '0' ); }          // 6.0.2.5 (2014/10/31) char を append する。
    1078                                rtn.append( mtm );
    1079                        }
    1080                }
    1081
    1082                return rtn.toString();
    1083        }
    1084
    1085        /**
    1086         * 表示項目の編集(並び替え)が可能かどうかを返します。
    1087         *
    1088         * @return      表示項目の編集(並び替え)が可能かどうか(false:不可能)
    1089         */
    1090        @Override
    1091        public boolean isEditable() {
    1092                return false;
    1093        }
    1094}