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