package logbook.gui.logic;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import logbook.dto.chart.Resource;
import logbook.dto.chart.ResourceLog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.widgets.Display;
import org.eclipse.wb.swt.SWTResourceManager;
/**
* 資材チャートを描画する
*
*/
public class ResourceChart {
/** タイムゾーンオフセット */
private static final long TZ_OFFSET = Calendar.getInstance().get(Calendar.ZONE_OFFSET);
/** グラフエリアの左マージン */
private static final int LEFT_WIDTH = 70;
/** グラフエリアの右マージン */
private static final int RIGHT_WIDTH = 45;
/** グラフエリアの上マージン */
private static final int TOP_HEIGHT = 30;
/** グラフエリアの下マージン */
private static final int BOTTOM_HEIGHT = 30;
/** 資材ログ */
private final ResourceLog log;
/** 期間 */
private final long term;
/** スケールテキスト */
private final String scaleText;
/** 刻み */
private final long notch;
/** Width */
private final int width;
/** Height */
private final int height;
private int max;
private int min;
private long[] time = {};
private Resource[] resources = {};
/**
* 資材チャート
*
* @param log 資材ログ
* @param scale 日単位のスケール
* @param width 幅
* @param height 高さ
*/
public ResourceChart(ResourceLog log, int scale, String scaleText, int width, int height) {
this.log = log;
this.term = TimeUnit.DAYS.toMillis(scale);
this.scaleText = scaleText;
this.notch = (long) (this.term / ((double) (width - LEFT_WIDTH - RIGHT_WIDTH) / 4));
this.width = width;
this.height = height;
// データロード
this.load();
}
/**
* グラフを描画します
*
* @param gc グラフィックコンテキスト
*/
public void draw(GC gc) {
// グラフエリアの幅
float w = this.width - LEFT_WIDTH - RIGHT_WIDTH;
// グラフエリアの高さ
float h = this.height - TOP_HEIGHT - BOTTOM_HEIGHT;
// お絵かき開始
gc.setAntialias(SWT.ON);
gc.setBackground(SWTResourceManager.getColor(SWT.COLOR_WHITE));
gc.fillRectangle(0, 0, this.width, this.height);
gc.setForeground(SWTResourceManager.getColor(SWT.COLOR_GRAY));
gc.setLineWidth(2);
// グラフエリアのラインを描く
// 縦
gc.drawLine(LEFT_WIDTH, TOP_HEIGHT, LEFT_WIDTH, this.height - BOTTOM_HEIGHT);
// 横
gc.drawLine(LEFT_WIDTH - 5, this.height - BOTTOM_HEIGHT, this.width - RIGHT_WIDTH, this.height - BOTTOM_HEIGHT);
// 縦軸を描く
gc.setLineWidth(1);
for (int i = 0; i < 5; i++) {
// 軸
gc.setForeground(SWTResourceManager.getColor(SWT.COLOR_GRAY));
int jh = (int) ((h * i) / 4) + TOP_HEIGHT;
gc.drawLine(LEFT_WIDTH - 5, jh, this.width - RIGHT_WIDTH, jh);
//ラベルを設定
gc.setForeground(SWTResourceManager.getColor(SWT.COLOR_BLACK));
String label = Integer.toString((int) (((float) (this.max - this.min) * (4 - i)) / 4) + this.min);
int labelWidth = getStringWidth(gc, label);
int labelHeight = gc.getFontMetrics().getHeight();
int x = LEFT_WIDTH - labelWidth - 5;
int y = jh - (labelHeight / 2);
gc.drawString(label, x, y);
}
SimpleDateFormat format = new SimpleDateFormat("M/d HH:mm");
// 横軸を描く
for (int i = 0; i < 5; i++) {
//ラベルを設定
gc.setForeground(SWTResourceManager.getColor(SWT.COLOR_BLACK));
int idx = (int) (((float) (this.time.length - 1) * i) / 4);
String label = format.format(new Date(normalizeTime(this.time[idx], TimeUnit.MINUTES.toMillis(10))));
int labelWidth = getStringWidth(gc, label);
int x = ((int) ((w * i) / 4) + LEFT_WIDTH) - (labelWidth / 2);
int y = (this.height - BOTTOM_HEIGHT) + 6;
gc.drawText(label, x, y, true);
}
// 判例を描く
int hx = LEFT_WIDTH;
int hy = 5;
for (int i = 0; i < this.resources.length; i++) {
gc.setLineWidth(3);
gc.setForeground(SWTResourceManager.getColor(this.resources[i].color));
String label = this.resources[i].name;
int labelWidth = getStringWidth(gc, label);
int labelHeight = gc.getFontMetrics().getHeight();
gc.drawLine(hx, hy + (labelHeight / 2), hx += 20, hy + (labelHeight / 2));
hx += 1;
gc.drawText(label, hx, hy, true);
hx += labelWidth + 2;
}
// スケールテキストを描く
int sx = this.width - RIGHT_WIDTH - getStringWidth(gc, this.scaleText);
int sy = 5;
gc.setForeground(SWTResourceManager.getColor(SWT.COLOR_BLACK));
gc.drawText(this.scaleText, sx, sy, true);
// グラフを描く
for (int i = 0; i < this.resources.length; i++) {
gc.setLineWidth(2);
gc.setForeground(SWTResourceManager.getColor(this.resources[i].color));
int[] values = this.resources[i].values;
Path path = new Path(Display.getCurrent());
float x = LEFT_WIDTH;
float y = (h * (1 - ((float) (values[0] - this.min) / (this.max - this.min)))) + TOP_HEIGHT;
path.moveTo(x, y);
for (int j = 1; j < values.length; j++) {
// 欠損(-1)データは描かない
if (values[j] != -1) {
float x1 = ((w * j) / values.length) + LEFT_WIDTH;
float y1 = (h * (1 - ((float) (values[j] - this.min) / (this.max - this.min)))) + TOP_HEIGHT;
path.lineTo(x1, y1);
}
}
gc.drawPath(path);
}
}
/**
* 資材ログを読み込む
*/
private void load() {
ResourceLog log = this.log;
// 時間はソートされている前提
// 最新の時間インデックス
int maxidx = log.time.length - 1;
// スケールで指定した範囲外で最も最新の時間インデックス、範囲外の時間がない場合0
int minidx = Math.max(Math.abs(Arrays.binarySearch(log.time, log.time[maxidx] - this.term)) - 2, 0);
// データを準備する
// データMax値
this.max = Integer.MIN_VALUE;
// データMin値
this.min = Integer.MAX_VALUE;
// グラフに必要なデータ配列の長さ
int length = (int) (this.term / this.notch) + 1;
// 時間軸
this.time = new long[length];
// グラフデータ(資材)
List<Resource> resourceList = new ArrayList<Resource>();
for (int i = 0; i < log.resources.length; i++) {
if (log.resources[i].color != null) {
resourceList.add(log.resources[i]);
}
}
this.resources = new Resource[resourceList.size()];
for (int i = 0; i < resourceList.size(); i++) {
this.resources[i] = new Resource(resourceList.get(i).name, resourceList.get(i).color, new int[length]);
}
// 時間を用意する
for (int i = 0; i < this.time.length; i++) {
this.time[i] = (log.time[maxidx] - this.term) + ((this.term / (length - 1)) * i);
}
// 資材を用意する
float fr = (float) (this.time[0] - log.time[minidx]) / (float) (log.time[minidx + 1] - log.time[minidx]);
long s = log.time[maxidx] - this.term;
for (int i = 0; i < this.resources.length; i++) {
// 補正前のデータ
int[] prevalues = resourceList.get(i).values;
// 補正されたスケールで指定した範囲のデータ
int[] values = this.resources[i].values;
// 初期値は-1(欠損)
Arrays.fill(values, -1);
if (log.time[minidx] <= this.time[0]) {
// スケール外データがある場合最初の要素を補完する
values[0] = (int) (prevalues[minidx] + ((prevalues[minidx + 1] - prevalues[minidx]) * fr));
}
for (int j = minidx + 1; j < prevalues.length; j++) {
int idx = (int) ((log.time[j] - s) / this.notch);
values[idx] = prevalues[j];
}
boolean find = false;
for (int j = 0; j < (length - 1); j++) {
// 先頭のデータがない場合0扱いにする
if (!find) {
if (values[j] >= 0) {
find = true;
} else {
values[j] = 0;
}
}
if (values[j] >= 0) {
// 資材最大数を設定
this.max = Math.max(values[j], this.max);
// 資材最小数を設定
this.min = Math.min(values[j], this.min);
}
}
}
// 資材の最大数を1000単位にする、資材の最大数が1000未満なら1000に設定
this.max = (int) Math.max(normalize(this.max, 1000), 1000);
// 資材の最小数を0.8でかけた後1000単位にする、
this.min = (int) Math.max(normalize((long) (this.min * 0.8f), 1000), 0);
}
/**
* 文字列のデバイス上の幅を返す
*
* @param gc GC
* @param str 文字列
* @return 文字列幅
*/
private static int getStringWidth(GC gc, String str) {
return gc.textExtent(str).x;
}
/**
* 数値を指定した間隔で刻む
*
* @param value 数値
* @param notch 刻み
* @return
*/
private static long normalize(long value, long notch) {
long t = value;
long half = notch / 2;
long mod = t % notch;
if (mod >= half) {
t += notch - mod;
} else {
t -= mod;
}
return t;
}
/**
* 時刻を指定した間隔で刻む
*
* @param time 時刻
* @param notch 刻み
* @return
*/
private static long normalizeTime(long time, long notch) {
return normalize(time + TZ_OFFSET, notch) - TZ_OFFSET;
}
}