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.hayabusa.io; 017 018import static org.opengion.fukurou.system.HybsConst.CR ; // 6.1.0.0 (2014/12/26) 019import org.opengion.fukurou.system.LogWriter; 020import org.opengion.fukurou.util.ColorMap; // 6.0.2.2 (2014/10/03) 021import org.opengion.fukurou.db.ResultSetValue; // 6.0.4.0 (2014/11/28) 022import org.opengion.hayabusa.db.DBTableModel; 023import org.opengion.hayabusa.common.HybsSystem; // 6.9.3.0 (2018/03/26)6.9.3.0 (2018/03/26) 024 025import java.sql.Connection; 026import java.sql.ResultSet; 027import java.sql.SQLException; 028import java.sql.Statement; 029 030import java.util.List; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Set; 034import java.util.HashSet; 035 036import java.awt.Color; // 6.0.2.2 (2014/10/03) 037 038import org.jfree.data.Range; 039import org.jfree.data.category.DefaultCategoryDataset; 040 041/** 042 * HybsCategoryDataset は、org.jfree.data.category.DefaultCategoryDataset を継承したサブクラスで、 043 * HybsDataset インターフェースの実装クラスになっています。 044 * これは、JDBCCategoryDatasetの データベース機能と、DBTableModel から Dataset を作成する機能を 045 * 兼ね備えています。 046 * HybsDataset インターフェースは、シリーズのラベル指定、カテゴリカラーバー、パレート図用積上げ 047 * 計算などの処理を行うための、インターフェースで、それらの処理も、HybsCategoryDataset に実装します。 048 * 049 * このクラスでは、検索結果を内部で持っておき、getValue(int row, int column) 050 * メソッドで直接値を返します。 051 * 052 * select category,series1,series2,series3,・・・ from ・・・ 053 * series の横持ち(標準と同じ) 対応です。 054 * category カラムの値は、カテゴリのラベルになり、series1,2,3 のラベルがシリーズラベル、値が 055 * seriesの値になります。 056 * 057 * カテゴリのカラー名の指定を行う場合、最後のカラムが、カラー名の文字列になります。 058 * select category,series1,series2,series3,・・・,color from ・・・ 059 * color文字列の検索結果は、Dataset には含まれません。 060 * 061 * その場合、color カラムがシリーズとして認識されない様に、ChartDatasetTag で、useCategoryColor="true" 062 * を指定しておく必要があります。このフラグは、HybsCategoryDataset を使う処理以外では効果が 063 * ありません(シリーズとして使用されてしまう)のでご注意ください。 064 * このフラグは、カテゴリカラーバーを使う場合には必要ですが、カテゴリカラーバーと(例えばパレート図) 065 * を合成する場合に、パレート図側にも useCategoryColor="true" を設定しておけば、同じSQL または、 066 * DBTableModel を使う事ができるというためのフラグです。 067 * 068 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。 069 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に 070 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース) 071 * このRenderer で、getItemPaint( int , int )メソッドをオーバーライドすることで、カテゴリごとの 072 * 色を返します。 073 * 074 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 075 * 076 * @version 6.0.2.2 (2014/10/03) 077 * @author Kazuhiko Hasegawa 078 * @since JDK1.6, 079 */ 080public class HybsCategoryDataset extends DefaultCategoryDataset implements HybsDataset { 081 private static final long serialVersionUID = 602220141003L ; 082 083 /** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ {@value} */ 084 private static final int DB_FETCH_SIZE = HybsSystem.sysInt( "DB_FETCH_SIZE" ) ; 085 086 private final Set<String> cateCheck = new HashSet<>(); // category の重複チェック 087 private final int hsCode = Long.valueOf( System.nanoTime() ).hashCode() ; // 5.1.9.0 (2010/08/01) equals,hashCode 088 089 private String[] seriesLabels ; 090 private boolean isColorCategory ; // 6.0.2.2 (2014/10/03) 091 private boolean isParetoData ; // 6.0.2.2 (2014/10/03) 092 093 private Number[][] numdata ; 094 private Color[] categoryColor ; 095 private Range range ; 096 097 /** 098 * デフォルトコンストラクター 099 * 100 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor. 101 */ 102 public HybsCategoryDataset() { super(); } // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。 103 104 /** 105 * CategoryDataset を構築するに当たり、初期パラメータを設定します。 106 * 107 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 108 * 109 * @param lbls シリーズのラベル名配列 110 * @param isColCate カテゴリのカラー名の指定有無(true:使用する) 111 * @param isPareto パレート図用のDatasetとして処理するかどうか(true:処理する) 112 */ 113 public void initParam( final String[] lbls , final boolean isColCate , final boolean isPareto ) { 114 // 6.0.2.5 (2014/10/31) refactoring 115 if( lbls != null ) { seriesLabels = lbls.clone(); } 116 isColorCategory = isColCate; 117 isParetoData = isPareto; 118 } 119 120 /** 121 * コネクションと、SQL文字列から、CategoryDataset のデータを作成します。 122 * 元となる処理は、org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String ) です。 123 * 124 * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して 125 * 検索した結果のデータを加工、処理します。 126 * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。 127 * 128 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 129 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 130 * @og.rev 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 131 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。 132 * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。 133 * 134 * @param conn コネクション 135 * @param query SQL文字列 136 * 137 * @throws SQLException データベースアクセス時のエラー 138 * @see org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String ) 139 * @see org.opengion.fukurou.db.ResultSetValue 140 */ 141 public void execute( final Connection conn, final String query ) throws SQLException { 142 143 // Range を予め求めておきます。 144 double minimum = Double.POSITIVE_INFINITY; 145 double maximum = Double.NEGATIVE_INFINITY; 146 double sum = 0.0d; // 6.0.2.3 (2014/10/19) パレート図用合計 147 148 List<Color> colorList = null; // 6.0.2.2 (2014/10/03) カテゴリカラー 149 150 // 6.4.2.1 (2016/02/05) try-with-resources 文 151 try( final Statement statement = conn.createStatement(); 152 final ResultSet resultSet = statement.executeQuery(query) ) { 153 154 statement.setFetchSize( DB_FETCH_SIZE ); // 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ 155 156 // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 157 final ResultSetValue rsv = new ResultSetValue( resultSet ); 158 159 int dataSize = rsv.getColumnCount() -1; // series の個数は、category 分を引いた数。 160 if( isColorCategory ) { // ColorCategory使用時 161 colorList = new ArrayList<>(); // カテゴリカラー 162 dataSize--; // 最終カラムが Colorコードなので、マイナスする。 163 } 164 165 if( dataSize<1 ) { 166 final String errMsg = "JDBCCategoryDataset.executeQuery() : insufficient columns " 167 + "returned from the database. \n" 168 + " SQL=" + query ; 169 throw new SQLException( errMsg ); 170 } 171 172 // 6.0.2.0 (2014/09/19) シリーズのラベル名配列を使うときは、シリーズ数必要。 173 if( seriesLabels != null && seriesLabels.length < dataSize ) { 174 final String errMsg = "seriesLabels を使用する場合は、必ずシリーズ数以上指定してください。" 175 + CR 176 + " seriesLabels=" + Arrays.toString( seriesLabels ) 177 + CR 178 + " seriesLabels.length=" + seriesLabels.length 179 + " dataSize=" + dataSize 180 + CR ; 181 throw new IllegalArgumentException( errMsg ); 182 } 183 184 String[] series = new String[dataSize]; 185 // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 186 final String[] names = rsv.getNames(); 187 // ORACLEの引数は、配列+1から始まるので、metaDataはi+2から取得。series と、seriesLabels は0から始まる。 188 for( int i=0; i<dataSize; i++ ) { 189 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..; 190 series[i] = seriesLabels == null || seriesLabels[i] == null 191 ? names[i+1] 192 : seriesLabels[i] ; 193 } 194 195 final List<Number[]> rowList = new ArrayList<>(); 196 // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 197 while( rsv.next() ) { 198 Number[] clmList = new Number[dataSize]; 199 // first column contains the row key... 200 // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。 201 final String category = uniqCategory( resultSet.getString(1) ); // 6.0.2.3 (2014/10/10) categoryの重複回避 202 203 for( int i=0; i<dataSize; i++ ) { // 6.0.2.2 (2014/10/03) dataSize 分回す。 204 Number value = null; 205 // 6.0.2.1 (2014/09/26) org.opengion.fukurou.db.DBUtil に、移動 206 try { 207 // JDBCのアドレス指定は、+2 する。(category 分と、アドレスが1から始まる為。) 208 // ResultSetValueのカラム番号は、+1 する。(category 分があるため) 209 value = rsv.getNumber( i+1 ); 210 } 211 catch( final SQLException ex ) { // 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。 212 LogWriter.log( ex ); 213 } 214 catch( final RuntimeException ex ) { 215 LogWriter.log( ex ); 216 } 217 218 clmList[i] = value; 219 addValue(value, series[i], category); // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。 220 // Range 求め 221 if( value != null ) { 222 final double dbl = value.doubleValue(); 223 if( isParetoData ) { // 6.0.2.3 (2014/10/19) パレート図用合計 224 sum += dbl ; 225 } else { 226 if( dbl < minimum ) { minimum = dbl; } 227 if( maximum < dbl ) { maximum = dbl; } 228 } 229 } 230 } 231 rowList.add( clmList ); 232 // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム 233 if( isColorCategory ) { 234 // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 235 final String colStr = rsv.getValue(dataSize+1); // 最後のカラム 236 final Color color = ColorMap.getColorInstance( colStr ); // 6.0.2.1 (2014/09/26) StringUtil → ColorMap 237 colorList.add( color ); 238 } 239 } 240 numdata = rowList.toArray( new Number[dataSize][rowList.size()] ); 241 } 242 243 // colorList が null でないかどうかで判定。 244 if( isColorCategory && colorList != null ) { 245 categoryColor = colorList.toArray( new Color[colorList.size()] ); 246 } 247 248 // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 249 if( isParetoData ) { 250 changeParetoData( sum ); 251 minimum = 0.0; 252 maximum = 100.0; 253 } 254 255 range = new Range( minimum, maximum ); 256 } 257 258 /** 259 * DBTableModelオブジェクトから、CategoryDataset のデータを作成します。 260 * openGionの独自処理メソッドです。 261 * 262 * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して 263 * 検索した結果のデータを加工、処理します。 264 * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。 265 * 266 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 267 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 268 * 269 * @param table DBTableModelオブジェクト 270 * @see #execute( Connection,String ) 271 */ 272 public void execute( final DBTableModel table ) { 273 final int clmNo = table.getColumnCount(); 274 final int rowNo = table.getRowCount(); 275 276 // Range を予め求めておきます。 277 double minimum = Double.POSITIVE_INFINITY; 278 double maximum = Double.NEGATIVE_INFINITY; 279 double sum = 0.0d; // 6.0.2.3 (2014/10/19) パレート図用合計 280 281 int dataSize = clmNo -1; // series の個数は、category 分を引いた数。 282 List<Color> colorList = null; // 6.0.2.2 (2014/10/03) カテゴリカラー 283 if( isColorCategory ) { // ColorCategory使用時 284 colorList = new ArrayList<>(); // カテゴリカラー 285 dataSize--; // 最終カラムが Colorコードなので、マイナスする。 286 } 287 288 numdata = new Number[rowNo][clmNo]; 289 290 // ※ DBTableModel の row,col と、Dataset の row,col は、逆になっています。 291 for( int row=0; row<rowNo; row++ ) { 292 final String category = uniqCategory( table.getValue( row,0 ) ); // 6.0.2.3 (2014/10/10) categoryの重複回避 293 final String[] vals = table.getValues( row ); 294 for( int clm=0; clm<dataSize; clm++ ) { 295 final String sval = vals[clm+1]; // 2番目(アドレス=1)からカラムデータを取得 296 final double val = sval == null || sval.isEmpty() ? 0.0d : Double.parseDouble( sval ) ; // 6.4.2.1 (2016/02/05) PMD refactoring. Useless parentheses. 297 298 addValue( val , seriesLabels[clm] , category ); // val,row,clm 299 numdata[row][clm] = Double.valueOf( val ); // 6.0.2.4 (2014/10/17) 効率の悪いメソッド 300 // Range 求め 301 if( isParetoData ) { // 6.0.2.3 (2014/10/19) パレート図用合計 302 sum += val ; 303 } else { 304 if( val < minimum ) { minimum = val; } 305 if( maximum < val ) { maximum = val; } 306 } 307 } 308 309 // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム 310 if( isColorCategory ) { 311 final String colStr = vals[dataSize+1]; // 最後のカラム 312 final Color color = ColorMap.getColorInstance( colStr ); // 6.0.2.1 (2014/09/26) StringUtil → ColorMap 313 colorList.add( color ); 314 } 315 } 316 317 // colorList が null でないかどうかで判定。 318 if( isColorCategory && colorList != null ) { 319 categoryColor = colorList.toArray( new Color[colorList.size()] ); 320 } 321 322 // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 323 if( isParetoData ) { 324 changeParetoData( sum ); 325 minimum = 0.0; 326 maximum = 100.0; 327 } 328 329 range = new Range( minimum, maximum ); 330 } 331 332 /** 333 * 指定された行列から、数字オブジェクトを取得します。 334 * 335 * @param row 行番号(シリーズ:横持=clm相当) 336 * @param column カラム番号(カテゴリ:縦持ち=row相当) 337 * 338 * @return 指定の行列の値 339 */ 340 @Override 341 public Number getValue( final int row, final int column ) { 342 // 注意:行列の順序が逆です。 343 return numdata[column][row]; 344 } 345 346 /** 347 * レンジオブジェクトを取得します。(独自メソッド) 348 * 349 * @return レンジオブジェクト 350 */ 351 public Range getRange() { 352 return range; 353 } 354 355 /** 356 * パレート図用のDatasetに値を書き換えます。(独自メソッド) 357 * 358 * 色々と方法はあると思いますが、簡易的に、内部の Number配列を 359 * 積上げ計算して、パレート図用のデータを作成します。 360 * レンジオブジェクト も変更します。 361 * 362 * ※ 注意:親クラスの内部に持っている実データは変更されていないので、 363 * 場合によっては、おかしな動きをするかもしれません。 364 * その場合は、上位にもデータをセットするように変更する必要があります。 365 * 366 * なお、行列の順序が、イメージと異なりますので、注意願います。 367 * (columnは、series , row は、category で、シリーズを積み上げます) 368 * 369 * @og.rev 6.0.2.1 (2014/09/26) 新規追加 370 * @og.rev 6.0.2.2 (2014/10/03) HybsDataset i/f 371 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 372 * 373 * @param sum データの合計 374 */ 375 private void changeParetoData( final double sum ) { 376 if( numdata == null || numdata.length == 0 || numdata[0].length == 0 || sum == 0.0 ) { return ; } 377 378 final int rowCnt = numdata[0].length ; 379 final int clmCnt = numdata.length ; 380 381 for( int rowNo=0; rowNo<rowCnt; rowNo++ ) { // 行列が逆。 382 double val = 0.0; // 初期値 383 for( int clmNo=0; clmNo<clmCnt; clmNo++ ) { // 積上げ計算するカラムでループを回す。 384 final Number v1Num = numdata[clmNo][rowNo]; 385 if( v1Num != null ) { 386 val += v1Num.doubleValue(); // 積上げ計算は、元の値のままにしておきます。 387 } 388 // データをセットするときに、100分率にします。 389 numdata[clmNo][rowNo] = Double.valueOf( Math.round( val * 1000.0 / sum ) / 10.0 ); 390 // きちんと計算するなら、BigDecimal で、スケールを指定して四捨五入すべき・・・かも 391 // java.math.BigDecimal bd = new BigDecimal( val * 100.0 / sum ); 392 // numdata[clmNo][rowNo] = bd.setScale( 1, java.math.RoundingMode.HALF_UP ); 393 } 394 } 395 396 } 397 398 /** 399 * categoryカラー配列を取得します。(独自メソッド) 400 * 401 * このクラスは、一番最後のカラムを、色文字列として処理し、categoryにColorを指定できます。 402 * select文で指定されていなかった場合は、null を返します。 403 * 404 * select category,series1,series2,series3,・・・,color from ・・・ 405 * 406 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 407 * 408 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。 409 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に 410 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース) 411 * このRenderer で、getItemPaint( int , int )メソッドをオーバーライドすることで、カテゴリごとの 412 * 色を返します。 413 * この設定を行うと、シリーズは、カテゴリと同一色になります。 414 * 415 * @return categoryカラー配列(なければ null) 416 */ 417 public Color[] getCategoryColor() { 418 // 6.0.2.5 (2014/10/31) refactoring 419 return ( categoryColor == null ) ? null : categoryColor.clone(); 420 } 421 422 /** 423 * category の重複をさけて、必要であれば、新しいカテゴリ名を作成します。 424 * 425 * カテゴリが同じ場合、JFreeChartでは、表示されません。これは、同じカテゴリと認識され 426 * 値が上書きされるためです。 427 * この問題は、なかなか気づきにくく、デバッグ等に時間がかかってしまいます。 428 * 重複チェックを行い、警告してもよいのですが、ここでは、新しいカテゴリ名を作成することで 429 * エラーを回避しつつ、とりあえずグラフ表示をするようにします。 430 * 431 * @og.rev 6.0.2.3 (2014/10/10) 新規追加 432 * 433 * @param category 元のカテゴリ名 434 * @return 新しい元のカテゴリ名 435 */ 436 private String uniqCategory( final String category ) { 437 String newCate = category ; 438 int i = 0; 439 while( !cateCheck.add( newCate ) ) { // すでに存在している場合。 440 newCate = category + "(" + (i++ ) + ")" ; 441 } 442 443 return newCate ; 444 } 445 446 /** 447 * この文字列と指定されたオブジェクトを比較します。 448 * 449 * 親クラスで、equals メソッドが実装されているため、警告がでます。 450 * 451 * @og.rev 5.1.8.0 (2010/07/01) findbug対応 452 * @og.rev 5.1.9.0 (2010/08/01) findbug対応 453 * 454 * @param object 比較するオブジェクト 455 * 456 * @return Objectが等しい場合は true、そうでない場合は false 457 */ 458 @Override 459 public boolean equals( final Object object ) { 460 // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method 461 return super.equals( object ) && hsCode == ((HybsCategoryDataset)object).hsCode; 462 463 } 464 465 /** 466 * このオブジェクトのハッシュコードを取得します。 467 * 468 * @og.rev 5.1.8.0 (2010/07/01) findbug対応 469 * @og.rev 5.1.9.0 (2010/08/01) findbug対応 470 * 471 * @return ハッシュコード 472 */ 473 @Override 474 public int hashCode() { return hsCode ; } 475}