package logbook.gui;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import logbook.config.AppConfig;
import logbook.constants.AppConstants;
import logbook.dto.chart.Resource;
import logbook.dto.chart.ResourceLog;
import logbook.dto.chart.ResourceLog.SortableLog;
import logbook.gui.logic.ColorManager;
import logbook.gui.logic.ResourceChart;
import logbook.gui.logic.ResourceChart.ActiveLevel;
import logbook.gui.logic.TableItemCreator;
import logbook.internal.LoggerHolder;
import logbook.scripting.TableItemCreatorProxy;
import logbook.util.SwtUtils;
import org.apache.commons.io.FilenameUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
/**
* 資材チャートのダイアログ
*
*/
public final class ResourceChartDialog extends WindowBase {
/** ロガー */
private static final LoggerHolder LOG = new LoggerHolder(ResourceChartDialog.class);
/** スケールテキスト */
private static final String[] SCALE_TEXT = { "1日", "1週間", "2週間", "1ヶ月", "2ヶ月", "3ヶ月", "半年", "1年" };
/** スケールテキストに対応する日 */
private static final int[] SCALE_DAYS = { 1, 7, 14, 30, 60, 90, 180, 365 };
/** 資材テーブルに表示する資材のフォーマット */
private static final String DIFF_FORMAT = "{0,number,0}({1,number,+0;-0})";
/** シェル */
private final Shell parent;
private Shell shell;
/** メニューバー */
private Menu menubar;
/** [ファイル]メニュー */
private Menu filemenu;
/** スケール */
private Combo combo;
/** グラフキャンバス */
private Canvas canvas;
/** 資材ログ */
private ResourceLog log;
/** 資材テーブル */
private Table table;
/** 資材テーブルのヘッダ */
private final String[] header = new String[] { "日付", "燃料", "弾薬", "鋼材", "ボーキ", "バーナー", "バケツ", "開発資材", "ネジ" };
/** 資材テーブルのボディ */
private List<String[]> body = new ArrayList<>();
/** 最後に読み込んだ時間 */
private Date lastLoadDate = new Date(0);
/** 更新タイマー */
protected Timer timer;
private int nextActivated = -1;
private int nowActivated = -1;
private Image currentImage;
private final Button[] enableCheckButtons = new Button[8];
/**
* Create the dialog.
* @param parent
*/
public ResourceChartDialog(Shell parent, MenuItem menuItem) {
super(menuItem);
this.parent = parent;
}
/**
* Open the dialog.
*/
@Override
public void open() {
// 初期化済みの場合
if (this.isWindowInitialized()) {
// リロードして表示
Calendar cal = Calendar.getInstance();
cal.setTime(this.lastLoadDate);
cal.add(Calendar.MINUTE, 1);
if (new Date().after(cal.getTime())) {
// 最後に読み込んでから1分以上経過していたら再読み込み
this.updateContents();
}
this.setVisible(true);
return;
}
this.createContents();
this.registerEvents();
this.setWindowInitialized(true);
this.setVisible(true);
// 更新タイマー
this.timer = new Timer(true);
// 10分毎に再読み込みするようにスケジュールする
this.timer.schedule(new CyclicReloadTask(), 0, TimeUnit.MINUTES.toMillis(10));
}
/**
* Create contents of the dialog.
*/
private void createContents() {
super.createContents(this.parent, SWT.SHELL_TRIM, false);
this.getShell().setText("資材チャート");
this.shell = this.getShell();
this.shell.setMinimumSize(450, 300);
this.shell.setSize(SwtUtils.DPIAwareSize(new Point(800, 650)));
this.shell.addListener(SWT.Dispose, new Listener() {
@Override
public void handleEvent(Event event) {
if (ResourceChartDialog.this.currentImage != null) {
ResourceChartDialog.this.currentImage.dispose();
ResourceChartDialog.this.currentImage = null;
}
}
});
GridLayout glShell = new GridLayout(1, false);
glShell.verticalSpacing = 2;
glShell.marginWidth = 2;
glShell.marginHeight = 2;
glShell.horizontalSpacing = 2;
this.shell.setLayout(glShell);
this.createMenubar();
this.menubar = this.getMenubar();
if (this.isNoMenubar()) {
this.filemenu = this.menubar;
}
else {
MenuItem fileroot = new MenuItem(this.menubar, SWT.CASCADE);
fileroot.setText("ファイル");
this.filemenu = new Menu(fileroot);
fileroot.setMenu(this.filemenu);
}
MenuItem save = new MenuItem(this.filemenu, SWT.NONE);
save.setText("画像ファイルとして保存(&S)\tCtrl+S");
save.setAccelerator(SWT.CTRL + 'S');
SashForm sashForm = new SashForm(this.shell, SWT.SMOOTH | SWT.VERTICAL);
sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
Composite compositeChart = new Composite(sashForm, SWT.NONE);
GridLayout glCompositeChart = new GridLayout(4, false);
glCompositeChart.verticalSpacing = 1;
glCompositeChart.marginWidth = 1;
glCompositeChart.marginHeight = 1;
glCompositeChart.marginBottom = 1;
glCompositeChart.horizontalSpacing = 1;
compositeChart.setLayout(glCompositeChart);
Label label = new Label(compositeChart, SWT.NONE);
label.setText("スケール");
this.combo = new Combo(compositeChart, SWT.READ_ONLY);
this.combo.setItems(SCALE_TEXT);
this.combo.select(2);
this.combo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
ResourceChartDialog.this.reloadImage();
}
});
Label label2 = new Label(compositeChart, SWT.NONE);
label2.setText("表示");
Composite enableCheckGroup = new Composite(compositeChart, SWT.NONE);
enableCheckGroup.setLayout(new RowLayout(SWT.HORIZONTAL));
SelectionListener checkboxListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
ResourceChartDialog.this.reloadImage();
}
};
final Runnable updateImage = new Runnable() {
@Override
public void run() {
ResourceChartDialog.this.updateActivatedImage();
}
};
MouseTrackListener checkboxMouseListener = new MouseTrackListener() {
@Override
public void mouseEnter(MouseEvent e) {
Control control = (Control) e.getSource();
ResourceChartDialog.this.nextActivated = (Integer) control.getData();
Display.getDefault().asyncExec(updateImage);
}
@Override
public void mouseExit(MouseEvent e) {
ResourceChartDialog.this.nextActivated = -1;
Display.getDefault().timerExec(100, updateImage);
}
@Override
public void mouseHover(MouseEvent e) {
}
};
String[] resourceNames = new String[] {
"燃料",
"弾薬",
"鋼材",
"ボーキ",
"バーナー",
"バケツ",
"開発資材",
"ネジ"
};
RGB[] colors = AppConfig.get().getResourceColors();
for (int i = 0; i < 8; ++i) {
Button check = new Button(enableCheckGroup, SWT.CHECK);
check.setData(i);
check.addSelectionListener(checkboxListener);
check.addMouseTrackListener(checkboxMouseListener);
check.setSelection(true);
// ButtonはWindowsだと色を変えられないので画像化して貼り付ける
String text = resourceNames[i];
GC gc = new GC(check);
Point textExtent = gc.stringExtent(text);
gc.dispose();
Image image = new Image(check.getDisplay(), new Rectangle(0, 0, textExtent.x, textExtent.y));
GC gcImage = new GC(image);
gcImage.setBackground(check.getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
gcImage.setForeground(ColorManager.getColor(colors[i]));
gcImage.drawText(text, 0, 0);
gcImage.dispose();
check.setImage(image);
this.enableCheckButtons[i] = check;
}
this.canvas = new Canvas(compositeChart, SWT.NO_BACKGROUND);
this.canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1));
Composite compositeTable = new Composite(sashForm, SWT.NONE);
GridLayout glCompositeTable = new GridLayout(1, false);
glCompositeTable.horizontalSpacing = 1;
glCompositeTable.marginHeight = 1;
glCompositeTable.marginWidth = 1;
glCompositeTable.verticalSpacing = 1;
compositeTable.setLayout(glCompositeTable);
this.table = new Table(compositeTable, SWT.BORDER | SWT.FULL_SELECTION);
this.table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
this.table.setHeaderVisible(true);
this.table.setLinesVisible(true);
sashForm.setWeights(new int[] { 3, 1 });
this.canvas.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
if (ResourceChartDialog.this.currentImage != null) {
e.gc.drawImage(ResourceChartDialog.this.currentImage, 0, 0);
}
}
});
this.canvas.addListener(SWT.Resize, new Listener() {
@Override
public void handleEvent(Event e) {
ResourceChartDialog.this.reloadImage();
}
});
// 画像ファイルとして保存のリスナー
save.addSelectionListener(new SaveImageAdapter());
// 資材テーブルを表示する
this.setTableHeader();
// データを読み込んで表示
this.updateContents();
}
private void updateActivatedImage() {
if (this.nowActivated != this.nextActivated) {
this.nowActivated = this.nextActivated;
this.reloadImage();
}
}
private void updateContents() {
File report = new File(FilenameUtils.concat(AppConfig.get().getReportPath(), AppConstants.LOG_RESOURCE));
try {
this.log = ResourceLog.getInstance(report);
if (this.log != null) {
this.body = createTableBody(this.log);
this.reloadImage();
this.setTableBody();
this.packTableHeader();
this.lastLoadDate = new Date();
}
} catch (IOException e) {
this.log = null;
}
}
private void reloadImage() {
int scale = SCALE_DAYS[ResourceChartDialog.this.combo.getSelectionIndex()];
String scaleText = "スケール:" + ResourceChartDialog.this.combo.getText();
Point size = ResourceChartDialog.this.canvas.getSize();
if ((size.x > 0) && (size.y > 0)) {
int width = size.x - 1;
int height = size.y - 1;
if (this.log != null) {
if (this.currentImage != null) {
this.currentImage.dispose();
}
this.currentImage = createImage(this.log, scale, scaleText, width, height,
ResourceChartDialog.this.getResourceActiveLevel(), false);
this.canvas.redraw();
}
}
}
/**
* テーブルヘッダーをセットする
*/
private void setTableHeader() {
for (int i = 0; i < this.header.length; i++) {
TableColumn col = new TableColumn(this.table, SWT.LEFT);
col.setText(this.header[i]);
}
this.packTableHeader();
}
/**
* テーブルヘッダーの幅を調節する
*/
private void packTableHeader() {
TableColumn[] columns = this.table.getColumns();
for (int i = 0; i < columns.length; i++) {
columns[i].pack();
}
}
/**
* テーブルボディーをセットする
*/
private void setTableBody() {
this.table.removeAll();
TableItemCreator creator = TableItemCreatorProxy.get(AppConstants.RESOURCECHAR_PREFIX);
creator.begin(this.header);
for (int i = 0; i < this.body.size(); i++) {
String[] line = this.body.get(i);
creator.create(this.table, line, i);
}
creator.end();
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
// マウスホイールイベントを期間選択コンボボックスに流すためにフォーカスする
this.combo.setFocus();
}
}
/**
* 資材ログのグラフイメージを作成する
*
* @param log 資材ログ
* @param scale 日単位のスケール
* @param width 幅
* @param height 高さ
* @return グラフイメージ
*/
private static Image createImage(ResourceLog log, int scale, String scaleText, int width, int height,
ActiveLevel[] activeLevel, boolean printHeader) {
Image image = new Image(Display.getCurrent(), Math.max(width, 1), Math.max(height, 1));
try {
GC gc = new GC(image);
try {
ResourceChart chart = new ResourceChart(
gc, log, scale, scaleText, width, height, activeLevel, printHeader);
chart.draw(gc);
} finally {
gc.dispose();
}
} catch (Exception e) {
image.dispose();
image = null;
LOG.get().warn("グラフの描画で例外が発生しました", e);
}
return image;
}
/**
* 資材テーブルのボディを作成する
*
* @param log 資材ログ
* @param body テーブルボディ
*/
private static List<String[]> createTableBody(ResourceLog log) {
List<String[]> body = new ArrayList<>();
SimpleDateFormat format = new SimpleDateFormat(AppConstants.DATE_DAYS_FORMAT);
format.setTimeZone(AppConstants.TIME_ZONE_MISSION);
Map<String, SortableLog> resourceofday = new LinkedHashMap<>();
for (int i = 0; i < log.time.length; i++) {
String key = format.format(new Date(log.time[i]));
Resource[] r = log.resources;
resourceofday.put(key, new SortableLog(log.time[i],
r[ResourceLog.RESOURCE_FUEL].values[i],
r[ResourceLog.RESOURCE_AMMO].values[i],
r[ResourceLog.RESOURCE_METAL].values[i],
r[ResourceLog.RESOURCE_BAUXITE].values[i],
r[ResourceLog.RESOURCE_BURNER].values[i],
r[ResourceLog.RESOURCE_BUCKET].values[i],
r[ResourceLog.RESOURCE_RESEARCH].values[i],
r[ResourceLog.RESOURCE_SCREW].values[i]));
}
MessageFormat diffFormat = new MessageFormat(DIFF_FORMAT);
SortableLog before = null;
for (Entry<String, SortableLog> entry : resourceofday.entrySet()) {
SortableLog val = entry.getValue();
int fuel = val.fuel;
int ammo = val.ammo;
int metal = val.metal;
int bauxite = val.bauxite;
int burner = val.burner;
int bucket = val.bucket;
int research = val.research;
int screw = val.screw;
int fuelDiff = fuel;
int ammoDiff = ammo;
int metalDiff = metal;
int bauxiteDiff = bauxite;
int burnerDiff = burner;
int bucketDiff = bucket;
int researchDiff = research;
int screwDiff = screw;
if (before != null) {
fuelDiff = fuel - before.fuel;
ammoDiff = ammo - before.ammo;
metalDiff = metal - before.metal;
bauxiteDiff = bauxite - before.bauxite;
burnerDiff = burner - before.burner;
bucketDiff = bucket - before.bucket;
researchDiff = research - before.research;
screwDiff = screw - before.screw;
}
before = val;
String[] line = new String[] {
entry.getKey(),
diffFormat.format(new Object[] { fuel, fuelDiff }),
diffFormat.format(new Object[] { ammo, ammoDiff }),
diffFormat.format(new Object[] { metal, metalDiff }),
diffFormat.format(new Object[] { bauxite, bauxiteDiff }),
diffFormat.format(new Object[] { burner, burnerDiff }),
diffFormat.format(new Object[] { bucket, bucketDiff }),
diffFormat.format(new Object[] { research, researchDiff }),
diffFormat.format(new Object[] { screw, screwDiff })
};
body.add(line);
}
Collections.reverse(body);
return body;
}
private ActiveLevel[] getResourceActiveLevel() {
ActiveLevel[] activated = new ActiveLevel[this.enableCheckButtons.length];
int nowActivated = ((this.nowActivated != -1) && this.enableCheckButtons[this.nowActivated].getSelection())
? this.nowActivated
: -1;
for (int i = 0; i < activated.length; ++i) {
boolean enabled = this.enableCheckButtons[i].getSelection();
if (!enabled) {
activated[i] = ActiveLevel.DISABLED;
}
else if (nowActivated == -1) {
activated[i] = ActiveLevel.NORMAL;
}
else if (nowActivated == i) {
activated[i] = ActiveLevel.ACTIVE;
}
else {
activated[i] = ActiveLevel.INACTIVE;
}
}
return activated;
}
private void saveImage() {
if (this.log != null) {
SimpleDateFormat format = new SimpleDateFormat(AppConstants.DATE_DAYS_FORMAT);
String name = "資材ログ_" + format.format(Calendar.getInstance().getTime()) + ".png";
FileDialog dialog = new FileDialog(this.shell, SWT.SAVE);
dialog.setFileName(name);
dialog.setFilterExtensions(new String[] { "*.png" });
String filename = dialog.open();
if (filename != null) {
File file = new File(filename);
if (file.exists()) {
MessageBox messageBox = new MessageBox(this.shell, SWT.YES | SWT.NO);
messageBox.setText("確認");
messageBox.setMessage("指定されたファイルは存在します。\n上書きしますか?");
if (messageBox.open() == SWT.NO) {
return;
}
}
int scale = SCALE_DAYS[this.combo.getSelectionIndex()];
String scaleText = "スケール:" + this.combo.getText();
Point size = this.canvas.getSize();
int width = size.x - 1;
int height = size.y - 1;
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
// イメージの生成
Image image = createImage(
this.log, scale, scaleText, width, height, this.getResourceActiveLevel(), true);
try {
ImageLoader loader = new ImageLoader();
loader.data = new ImageData[] { image.getImageData() };
loader.save(out, SWT.IMAGE_PNG);
} finally {
image.dispose();
}
} catch (Exception ex) {
MessageBox messageBox = new MessageBox(this.shell, SWT.ICON_ERROR);
messageBox.setText("書き込めませんでした");
messageBox.setMessage(ex.toString());
messageBox.open();
}
}
}
}
/**
* 画像ファイルとして保存のリスナー
*
*/
private final class SaveImageAdapter extends SelectionAdapter {
@Override
public void widgetSelected(SelectionEvent e) {
ResourceChartDialog.this.saveImage();
}
}
/**
* テーブルを定期的に再読み込みする
*/
protected class CyclicReloadTask extends TimerTask {
@Override
public void run() {
ResourceChartDialog.this.shell.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!ResourceChartDialog.this.shell.isDisposed()) {
// 見えているときだけ処理する
if (ResourceChartDialog.this.shell.isVisible()) {
ResourceChartDialog.this.updateContents();
}
}
else {
// ウインドウが消えていたらタスクをキャンセルする
CyclicReloadTask.this.cancel();
}
}
});
}
}
}