package logbook.internal.gui; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.chart.BarChart; import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import logbook.internal.BattleLogs; import logbook.internal.BattleLogs.SimpleBattleLog; /** * 経験値チャート * */ public class ExpChartController extends WindowController { @FXML private ChoiceBox<TypeOption> type; @FXML private ChoiceBox<ScaleOption> term; @FXML private CheckBox forceZero; @FXML private CheckBox stacked; @FXML private BarChart<String, Number> chart; @FXML private CategoryAxis xAxis; @FXML private NumberAxis yAxis; @FXML void initialize() { // 選択肢を追加 this.type.setItems(FXCollections.observableArrayList(TypeOption.values())); this.term.setItems(FXCollections.observableArrayList(ScaleOption.values())); this.type.getSelectionModel().select(0); this.term.getSelectionModel().select(2); this.type.getSelectionModel().selectedItemProperty().addListener(this::changed); this.term.getSelectionModel().selectedItemProperty().addListener(this::changed); this.change(); } @FXML void change(ActionEvent event) { this.change(); } @FXML void forceZeroChange(ActionEvent event) { this.yAxis.setForceZeroInRange(this.forceZero.isSelected()); } private void changed(ObservableValue<?> observable, Object oldValue, Object Object) { this.change(); } /** * 選択肢が変更された時の処理 */ private void change() { TypeOption type = this.type.getSelectionModel().getSelectedItem(); ScaleOption scale = this.term.getSelectionModel().getSelectedItem(); ZonedDateTime baseDate = scale.convert(ZonedDateTime.now(), type); ZonedDateTime min = scale.min(baseDate); ZonedDateTime max = scale.max(baseDate); Map<ZonedDateTime, Double> data = this.load(type, scale, min, max); XYChart.Series<String, Number> series = new XYChart.Series<>(); series.setName(type.toString()); boolean stacked = this.stacked.isSelected(); ObservableList<String> categories = FXCollections.observableArrayList(); double current = 0D; for (Entry<ZonedDateTime, Double> entry : data.entrySet()) { String key = scale.getFormat().format(entry.getKey().withZoneSameInstant(ZoneId.of("Asia/Tokyo"))); if (stacked) { current += entry.getValue(); } else { current = entry.getValue(); } categories.add(key); series.getData().add(new XYChart.Data<>(key, current)); } this.chart.setTitle(type + "(" + scale + ")"); this.chart.getData().clear(); this.xAxis.getCategories().clear(); this.xAxis.setCategories(categories); this.chart.getData().add(series); } /** * グラフデータを読み込み * * @param type 種類 * @param scale 期間 * @param min 期間の最小(自身を含む) * @param max 期間の最大(自身を含まない) * @return グラフデータ */ private Map<ZonedDateTime, Double> load(TypeOption type, ScaleOption scale, ZonedDateTime min, ZonedDateTime max) { Map<ZonedDateTime, Double> map = new LinkedHashMap<>(); // 空のデータを作る ZonedDateTime current = min; while (current.compareTo(max) < 0) { map.put(current, 0D); current = current.plus(scale.getTick()); } // ログから読み込み Instant minInstant = min.toInstant(); Instant maxInstant = max.toInstant(); List<SimpleBattleLog> logs = BattleLogs.readSimpleLog(log -> { Instant a = log.getDate().toInstant(); return a.compareTo(minInstant) >= 0 && a.compareTo(maxInstant) < 0; }); map.putAll(logs.stream() .collect(Collectors.groupingBy(log -> scale.convert(log.getDate(), type), Collectors.summingDouble(type::convert)))); return map; } /** * 種類 * */ private enum TypeOption { SHIP_EXP("艦娘経験値") { @Override public double convert(SimpleBattleLog log) { String str = log.getShipExp(); if (str == null || str.isEmpty()) return 0D; return Double.parseDouble(str); } }, EXP("提督経験値") { @Override public double convert(SimpleBattleLog log) { String str = log.getExp(); if (str == null || str.isEmpty()) return 0D; return Double.parseDouble(str); } }, SENKA("戦果") { @Override public double convert(SimpleBattleLog log) { String str = log.getExp(); if (str == null || str.isEmpty()) return 0D; return Double.parseDouble(str) / 1428.571D; } }; private String name; private TypeOption(String name) { this.name = name; } public double convert(SimpleBattleLog log) { throw new UnsupportedOperationException(); } @Override public String toString() { return this.name; } } /** * スケールの選択肢 * */ private enum ScaleOption { /** 今日 */ NOW_DAY("今日", "HH:mm", Duration.ofHours(1)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.HOURS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .plusDays(1); } }, /** 昨日 */ LAST_DAY("昨日", "HH:mm", Duration.ofHours(1)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.HOURS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .minusDays(1); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS); } }, /** 今週 */ NOW_WEEK("今週", "d日a", Duration.ofHours(12)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.HALF_DAYS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .minusDays(base.getDayOfWeek().getValue() - 1); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .plusWeeks(1) .minusDays(base.getDayOfWeek().getValue() - 1); } }, /** 先週 */ LAST_WEEK("先週", "d日a", Duration.ofHours(12)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.HALF_DAYS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .minusWeeks(1) .minusDays(base.getDayOfWeek().getValue() - 1); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .minusDays(base.getDayOfWeek().getValue() - 1); } }, /** 今月 */ NOW_MONTH("今月", "d日", Duration.ofDays(1)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.DAYS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .withDayOfMonth(1); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .withDayOfMonth(1) .plusMonths(1); } }, /** 先月 */ LAST_MONTH("先月", "d日", Duration.ofDays(1)) { @Override public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { return super.convert(time, type).truncatedTo(ChronoUnit.DAYS); } @Override public ZonedDateTime min(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .withDayOfMonth(1) .minusMonths(1); } @Override public ZonedDateTime max(ZonedDateTime base) { return base.truncatedTo(ChronoUnit.DAYS) .withDayOfMonth(1); } }; private String name; private DateTimeFormatter format; private Duration tick; ScaleOption(String name, String format, Duration tick) { this.name = name; this.format = DateTimeFormatter.ofPattern(format); this.tick = tick; } public ZonedDateTime convert(ZonedDateTime time, TypeOption type) { // 戦果を選んだ場合日本時間午前2時が0時になるタイムゾーンを使用する if (type == TypeOption.SENKA) { return time.withZoneSameInstant(ZoneId.of("UTC+07:00")); } else { return time.withZoneSameInstant(ZoneId.of("Asia/Tokyo")); } } public ZonedDateTime min(ZonedDateTime base) { throw new UnsupportedOperationException(); } public ZonedDateTime max(ZonedDateTime base) { throw new UnsupportedOperationException(); } public DateTimeFormatter getFormat() { return this.format; } public Duration getTick() { return this.tick; } @Override public String toString() { return this.name; } } }