/*
* Copyright (C) 2014-2015 ULYSSIS VZW
*
* This file is part of i++.
*
* i++ is free software: you can redistribute it and/or modify
* it under the terms of version 3 of the GNU Affero General Public License
* as published by the Free Software Foundation. No other versions apply.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package org.ulyssis.ipp.ui.widgets;
import eu.webtoolkit.jwt.ItemDataRole;
import eu.webtoolkit.jwt.Orientation;
import eu.webtoolkit.jwt.TextFormat;
import eu.webtoolkit.jwt.WAbstractItemModel;
import eu.webtoolkit.jwt.WContainerWidget;
import eu.webtoolkit.jwt.WLineEdit;
import eu.webtoolkit.jwt.WMenu;
import eu.webtoolkit.jwt.WModelIndex;
import eu.webtoolkit.jwt.WProgressBar;
import eu.webtoolkit.jwt.WPushButton;
import eu.webtoolkit.jwt.WSpinBox;
import eu.webtoolkit.jwt.WStackedWidget;
import eu.webtoolkit.jwt.WString;
import eu.webtoolkit.jwt.WTemplate;
import eu.webtoolkit.jwt.WWidget;
import eu.webtoolkit.jwt.chart.Axis;
import eu.webtoolkit.jwt.chart.ChartType;
import eu.webtoolkit.jwt.chart.SeriesType;
import eu.webtoolkit.jwt.chart.WCartesianChart;
import eu.webtoolkit.jwt.chart.WDataSeries;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ulyssis.ipp.TagId;
import org.ulyssis.ipp.config.Config;
import org.ulyssis.ipp.config.Team;
import org.ulyssis.ipp.control.commands.AddTagCommand;
import org.ulyssis.ipp.control.commands.CorrectionCommand;
import org.ulyssis.ipp.control.commands.RemoveTagCommand;
import org.ulyssis.ipp.processor.Database;
import org.ulyssis.ipp.publisher.Score;
import org.ulyssis.ipp.snapshot.Snapshot;
import org.ulyssis.ipp.snapshot.Event;
import org.ulyssis.ipp.snapshot.TagSeenEvent;
import org.ulyssis.ipp.ui.UIApplication;
import org.ulyssis.ipp.ui.state.SharedState;
import org.ulyssis.ipp.utils.Serialization;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
public class TeamPanel extends CollapsablePanel {
private static final Logger LOG = LogManager.getLogger(TeamPanel.class);
private final Team team;
private final WTemplate barContent;
private final WContainerWidget content;
private final WMenu menu;
private final WStackedWidget stack;
private final WContainerWidget chartContainer;
private final WTemplate tagsView;
private final WTemplate correctionsView;
private final WContainerWidget tagsTable;
private final WLineEdit addTagEdit;
private final WPushButton addTagButton;
private final WSpinBox correctionSpinner;
private final WPushButton addCorrection;
private final WProgressBar projectedProgress;
private final WProgressBar actualProgress;
private UIApplication application;
private final SharedState sharedState;
private final List<WCartesianChart> charts = new ArrayList<>();
private final List<EventsModel> itemModels = new ArrayList<>();
private Optional<Snapshot> latestSnapshot = Optional.empty();
private final Config config;
private static final class EventsModel extends WAbstractItemModel {
private List<TagSeenEvent> events = Collections.emptyList();
private Instant oneHourAgo = Instant.now().minus(1L, ChronoUnit.HOURS);
public void setEvents(Instant oneHourAgo, List<TagSeenEvent> events) {
this.oneHourAgo = oneHourAgo;
this.events = events;
this.reset();
}
@Override
public int getColumnCount(WModelIndex parent) {
return 2;
}
@Override
public int getRowCount(WModelIndex parent) {
int res = Math.max(0, events.size() - 1);
return res;
}
@Override
public WModelIndex getParent(WModelIndex wModelIndex) {
return null;
}
@Override
public Object getData(WModelIndex index, int role) {
if (events.size() < 2) {
LOG.error("Events size is wrong!");
return null;
}
if (role == ItemDataRole.DisplayRole) {
if (index.getRow() >= events.size() - 1 || index.getColumn() >= 2) {
LOG.error("Returning null!");
return null;
}
if (index.getColumn() == 0) {
return -events.size() + 1 + index.getRow();
} else if (index.getColumn() == 1) {
double res = Duration.between(events.get(index.getRow()).getTime(), events.get(index.getRow() + 1).getTime()).toMillis() / 1000D;
return res;
}
}
return null;
}
@Override
public WModelIndex getIndex(int row, int col, WModelIndex wModelIndex) {
return createIndex(row, col, null);
}
}
private final SharedState.SnapshotScoreListener scoreListener = this::processNewScore;
private void processNewScore(Snapshot snapshot, Score score, boolean newSnapshot) {
updateTags(snapshot);
latestSnapshot = Optional.of(snapshot);
try {
barContent.bindInt("rounds", snapshot.getTeamStates().getNbLapsForTeam(team.getTeamNb()));
int i = 0;
for (Score.Team team : score.getTeams()) {
if (team.getNb() == this.team.getTeamNb()) {
break;
}
i ++;
}
int ranking = i + 1;
barContent.bindInt("pos", ranking);
score.getTeams().stream().filter(team -> team.getNb() == this.team.getTeamNb()).findFirst().ifPresent(scoreTeam -> {
snapshot.getTeamStates().getStateForTeam(this.team.getTeamNb()).ifPresent(teamState -> {
double pos = teamState.getLastTagSeenEvent().map(ev -> config.getReader(ev.getReaderId()).getPosition()).orElse(0.0);
actualProgress.setValue(pos / config.getTrackLength());
projectedProgress.setValue(scoreTeam.getPosition());
if (scoreTeam.getNonLimitedPosition() > 1.0) {
double alpha = (scoreTeam.getNonLimitedPosition() - 1.0) * 4;
// TODO: Use the alpha to indicate how severe the delay is
}
if (getState() == State.Extended && newSnapshot && menu.getCurrentIndex() == 0) {
updateCharts();
}
});
});
} catch (Exception e) {
LOG.error("Exception", e);
}
}
private Set<TagId> oldTags = new HashSet<>();
private void updateTags(Snapshot snapshot) {
Set<TagId> newTags = new HashSet<>();
snapshot.getTeamTagMap().getTagToTeam().forEach((tag, teamNb) -> {
if (team.getTeamNb() == teamNb) {
newTags.add(tag);
}
});
if (!newTags.equals(oldTags)) {
oldTags = newTags;
tagsTable.clear();
for (TagId tag : newTags) {
WTemplate tagsTableItem = new WTemplate(WString.tr("tags-table-item"));
tagsTableItem.bindString("tag-id", tag.toString());
final WPushButton removeTagButton = new WPushButton("Remove");
tagsTableItem.bindWidget("remove-button", removeTagButton);
removeTagButton.clicked().addListener(this, () -> removeTag(tag, removeTagButton));
tagsTable.addWidget(tagsTableItem);
}
}
}
private void removeTag(TagId tag, WPushButton removeButton) {
sharedState.getCommandDispatcher().sendAsync(new RemoveTagCommand(tag, team.getTeamNb()));
}
// TODO: This shouldn't be entirely based on the latest snapshot... also, share this info!
private void updateCharts() {
Instant now = Instant.now();
Instant anHourAgo = now.minus(Duration.ofHours(1L));
try (Connection connection = Database.createConnection(EnumSet.of(Database.ConnectionFlags.READ_ONLY))) {
List<Event> events = Event.loadFrom(connection, anHourAgo);
if (events.size() == 0) {
LOG.warn("No events");
return;
}
Snapshot snapshot = Snapshot.loadForEvent(connection, events.get(0)).get(); // If it's not there, this is a fatal error
List<Optional<Instant>> eventTimes = new ArrayList<>();
List<List<TagSeenEvent>> eventLists = new ArrayList<>();
for (int i = 0; i < config.getNbReaders(); i++) {
eventTimes.add(Optional.empty());
eventLists.add(new ArrayList<>());
}
for (int i = 1; i < events.size(); ++i) {
Event event = events.get(i);
snapshot = event.apply(snapshot);
if (event instanceof TagSeenEvent && !event.isRemoved()) { // TODO(Roel): Well, we can't actually remove it, right?
final TagSeenEvent tagSeenEvent = (TagSeenEvent)event;
Optional<Integer> teamNb = snapshot.getTeamTagMap().tagToTeam(tagSeenEvent.getTag());
if (teamNb.isPresent() && teamNb.get().equals(team.getTeamNb())) {
// TODO(Roel): This hack is kind of dirty. In fact, all of this
// code is kind of dirty.
if (eventTimes.get(tagSeenEvent.getReaderId()).isPresent()) {
if (Duration.between(eventTimes.get(tagSeenEvent.getReaderId()).get(),
event.getTime()).toMillis() <= 30_000) {
continue;
}
}
eventTimes.get(tagSeenEvent.getReaderId()).ifPresent(lastTime -> {
try {
eventLists.get(tagSeenEvent.getReaderId()).add(tagSeenEvent);
} catch (Exception e) {
LOG.error("Exception", e);
}
});
eventTimes.set(tagSeenEvent.getReaderId(), Optional.of(event.getTime()));
}
}
}
for (int i = 0; i < config.getNbReaders(); i++) {
List<TagSeenEvent> eventList = eventLists.get(i);
List<TagSeenEvent> shortenedEventList = new ArrayList<>();
if (eventList.size() > 40) {
for (int j = eventList.size() - 40; j < eventList.size(); j++) {
shortenedEventList.add(eventList.get(j));
}
} else {
shortenedEventList = eventList;
}
itemModels.get(i).setEvents(anHourAgo, shortenedEventList);
}
} catch (SQLException e) {
LOG.error("Couldn't fetch events", e);
} catch (IOException e) {
LOG.error("Error processing events", e);
}
}
public TeamPanel(Team team) {
this(team, null);
}
public TeamPanel(Team team, WContainerWidget parent) {
super(parent);
this.team = team;
application = UIApplication.getInstance();
config = Config.getCurrentConfig();
sharedState = application.getSharedState();
barContent = new WTemplate(WString.tr("team-bar"));
content = new WContainerWidget();
stack = new WStackedWidget();
stack.addStyleClass("teams-stack");
menu = new WMenu(stack, Orientation.Horizontal);
menu.addStyleClass("teams-menu");
chartContainer = new WContainerWidget();
chartContainer.addStyleClass("teams-charts");
tagsView = new WTemplate(WString.tr("tags-view"));
tagsView.addStyleClass("tags-view");
correctionsView = new WTemplate(WString.tr("corrections-view"));
correctionsView.addStyleClass("corrections-view");
menu.addItem("Charts", chartContainer);
menu.addItem("Tags", tagsView);
menu.addItem("Corrections", correctionsView);
content.addWidget(menu);
content.addWidget(stack);
barContent.bindInt("nb", team.getTeamNb());
barContent.bindString("name", team.getName(), TextFormat.PlainText);
barContent.bindInt("pos", team.getTeamNb());
barContent.bindInt("rounds", 0);
projectedProgress = new WProgressBar();
projectedProgress.setStyleClass("projected-progress");
projectedProgress.setMinimum(0.0);
projectedProgress.setMaximum(1.0);
barContent.bindWidget("projected-progress", projectedProgress);
actualProgress = new WProgressBar();
actualProgress.setStyleClass("actual-progress");
actualProgress.setMinimum(0.0);
actualProgress.setMaximum(1.0);
barContent.bindWidget("actual-progress", actualProgress);
for (int i = 0; i < config.getNbReaders(); i++) {
WCartesianChart readerChart = new WCartesianChart(ChartType.ScatterPlot, chartContainer);
readerChart.addStyleClass("reader-chart");
EventsModel itemModel = new EventsModel();
readerChart.setModel(itemModel);
WDataSeries series = new WDataSeries(1, SeriesType.BarSeries);
readerChart.addSeries(series);
readerChart.resize(300, 200);
readerChart.getAxis(Axis.XAxis).setRange(-40, 0);
readerChart.getAxis(Axis.YAxis).setMinimum(0);
readerChart.setXSeriesColumn(0);
readerChart.setAutoLayoutEnabled(true);
charts.add(readerChart);
itemModels.add(itemModel);
}
tagsTable = new WContainerWidget();
tagsView.bindWidget("tags-table", tagsTable);
addTagEdit = new WLineEdit();
tagsView.bindWidget("add-tag-id", addTagEdit);
addTagButton = new WPushButton("Add");
tagsView.bindWidget("add-tag-button", addTagButton);
addTagButton.clicked().addListener(this, this::addTag);
correctionSpinner = new WSpinBox();
correctionSpinner.setMinimum(-2000);
correctionSpinner.setMaximum(2000);
correctionsView.bindWidget("correction-spinner", correctionSpinner);
addCorrection = new WPushButton("Commit");
correctionsView.bindWidget("commit-button", addCorrection);
addCorrection.clicked().addListener(this, this::performCorrection);
sharedState.addScoreListener(application, scoreListener);
}
private void addTag() {
String tagIdString = addTagEdit.getText();
addTagEdit.setText("");
try {
TagId tag = new TagId(tagIdString);
sharedState.getCommandDispatcher().sendAsync(new AddTagCommand(tag, team.getTeamNb()));
} catch (IllegalArgumentException e) {
// TODO: Handle decoding exception!
}
}
private void performCorrection() {
int correction = correctionSpinner.getValue();
correctionSpinner.setValue(0);
sharedState.getCommandDispatcher().sendAsync(new CorrectionCommand(team.getTeamNb(), correction));
}
@Override
public void remove() {
sharedState.removeScoreListener(application, scoreListener);
super.remove();
}
@Override
protected WWidget barContentWidget() {
return barContent;
}
@Override
protected WWidget contentWidget() {
return content;
}
@Override
protected void toggleOpenClosed() {
super.toggleOpenClosed();
if (latestSnapshot.isPresent()) {
updateCharts();
}
}
}