/* * Project Sc2gears * * Copyright (c) 2010 Andras Belicza <[email protected]> * * This software is the property of Andras Belicza. * Copying, modifying, distributing, refactoring without the authors permission * is prohibited and protected by Law. */ package hu.belicza.andras.sc2gears.ui.moduls.multirepanal; import static hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.FRAME_BITS_IN_SECOND; import hu.belicza.andras.sc2gears.Consts; import hu.belicza.andras.sc2gears.language.Language; import hu.belicza.andras.sc2gears.sc2replay.EnumCache; import hu.belicza.andras.sc2gears.sc2replay.ReplayUtils; import hu.belicza.andras.sc2gears.sc2replay.model.Details; import hu.belicza.andras.sc2gears.sc2replay.model.Details.Player; import hu.belicza.andras.sc2gears.sc2replay.model.Details.PlayerId; import hu.belicza.andras.sc2gears.sc2replay.model.GameEvents.Action; import hu.belicza.andras.sc2gears.sc2replay.model.GameEvents.BuildAction; import hu.belicza.andras.sc2gears.sc2replay.model.Replay; import hu.belicza.andras.sc2gears.settings.Settings; import hu.belicza.andras.sc2gears.ui.GuiUtils; import hu.belicza.andras.sc2gears.ui.MainFrame; import hu.belicza.andras.sc2gears.ui.charts.ChartUtils.GraphApproximation; import hu.belicza.andras.sc2gears.ui.components.FirstShownListener; import hu.belicza.andras.sc2gears.ui.components.PlayerPopupMenu; import hu.belicza.andras.sc2gears.ui.components.TableBox; import hu.belicza.andras.sc2gears.ui.dialogs.MiscSettingsDialog; import hu.belicza.andras.sc2gears.ui.dialogs.MiscSettingsDialog.SettingsTab; import hu.belicza.andras.sc2gears.ui.icons.Icons; import hu.belicza.andras.sc2gears.ui.moduls.ModuleFrame; import hu.belicza.andras.sc2gears.ui.moduls.replaysearch.ReplaySearch; import hu.belicza.andras.sc2gears.util.GeneralUtils; import hu.belicza.andras.sc2gears.util.Holder; import hu.belicza.andras.sc2gears.util.NormalThread; import hu.belicza.andras.sc2gears.util.NullAwareComparable; import hu.belicza.andras.sc2gears.util.ReplayCache; import hu.belicza.andras.sc2gearspluginapi.api.enums.League; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.Building; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.Format; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.GameSpeed; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.GameType; import hu.belicza.andras.sc2gearspluginapi.api.sc2replay.ReplayConsts.Race; import hu.belicza.andras.sc2gearspluginapi.impl.util.IntHolder; import hu.belicza.andras.sc2gearspluginapi.impl.util.Pair; import hu.belicza.andras.sc2gearspluginapi.impl.util.WordCloudTableInput; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.EnumMap; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JProgressBar; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.SortOrder; import javax.swing.SwingUtilities; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; import javax.swing.event.InternalFrameListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; /** * Multi-replay analysis. * * @author Andras Belicza */ @SuppressWarnings("serial") public class MultiRepAnalysis extends ModuleFrame { /** Text keys of the days of the week. */ private static final String[] DAY_TEXT_KEYS = { "general.day.sunday", "general.day.monday", "general.day.tuesday", "general.day.wednesday", "general.day.thursday", "general.day.friday", "general.day.saturday" }; /** Text keys of the months. */ private static final String[] MONTH_TEXT_KEYS = { "general.month.january", "general.month.february", "general.month.march", "general.month.april", "general.month.may", "general.month.june", "general.month.july", "general.month.august", "general.month.september", "general.month.october", "general.month.november", "general.month.december" }; /** * Chart type. * @author Andras Belicza */ public static enum ChartType { ACTIVITY ( "module.multiRepAnal.tab.player.tab.charts.chartType.activity" , "module.multiRepAnal.tab.player.tab.charts.chartType.activity.yAxisLabel" ), APM_DEVELOPMENT ( "module.multiRepAnal.tab.player.tab.charts.chartType.apmDevelopment" , "module.multiRepAnal.tab.player.tab.charts.chartType.apmDevelopment.yAxisLabel" ), WIN_RATIO_DEVELOPMENT ( "module.multiRepAnal.tab.player.tab.charts.chartType.winRatioDevelopment" , "module.multiRepAnal.tab.player.tab.charts.chartType.winRatioDevelopment.yAxisLabel" ), SPAWN_LARVA_DEVELOPMENT( "module.multiRepAnal.tab.player.tab.charts.chartType.spawnLarvaDevelopment", "module.multiRepAnal.tab.player.tab.charts.chartType.spawnLarvaDevelopment.yAxisLabel" ), RACE_DISTRIBUTION ( "module.multiRepAnal.tab.player.tab.charts.chartType.raceDistribution" , "module.multiRepAnal.tab.player.tab.charts.chartType.raceDistribution.yAxisLabel#2" ), GAME_TYPE_DISTRIBUTION ( "module.multiRepAnal.tab.player.tab.charts.chartType.gameTypeDistribution" , "module.multiRepAnal.tab.player.tab.charts.chartType.gameTypeDistribution.yAxisLabel" ), FORMAT_DISTRIBUTION ( "module.multiRepAnal.tab.player.tab.charts.chartType.formatDistribution" , "module.multiRepAnal.tab.player.tab.charts.chartType.formatDistribution.yAxisLabel" ); /** Cache of the string value. */ public final String stringValue; /** Key of Y axis label of this chart type. */ public final String yAxisLabelKey; /** Key stroke for fast accessing this chart type. */ public final KeyStroke keyStroke; /** * Creates a new ChartType. * @param textKey key of the text representation * @param yAxisLabelKey key of Y axis label of this chart type */ private ChartType( final String textKey, final String yAxisLabelKey ) { this.yAxisLabelKey = yAxisLabelKey; final int chartNumber = ordinal() + 1; keyStroke = chartNumber > 10 ? null : KeyStroke.getKeyStroke( KeyEvent.VK_0 + chartNumber, InputEvent.CTRL_MASK ); // CTRL+1 for chart type 1, CTRL+2 select chart type 2 etc. if ( keyStroke == null ) stringValue = Language.getText( textKey ); else stringValue = "<html>" + Language.getText( textKey ) + " <font color=#777777>Ctrl+" + ( chartNumber == 10 ? 0 : chartNumber ) + "</font></html>"; } @Override public String toString() { return stringValue; }; } /** * Chart type. * @author Andras Belicza */ public static enum ChartGranularity { DAY ( "module.multiRepAnal.tab.player.tab.charts.granularity.day" ), WEEK ( "module.multiRepAnal.tab.player.tab.charts.granularity.week" ), MONTH ( "module.multiRepAnal.tab.player.tab.charts.granularity.month" ), YEAR ( "module.multiRepAnal.tab.player.tab.charts.granularity.year" ), LADDER_SEASON( "module.multiRepAnal.tab.player.tab.charts.granularity.ladderSeason" ); /** Cache of the string value. */ private final String stringValue; /** * Creates a new ChartType. * @param textKey key of the text representation */ private ChartGranularity( final String textKey ) { stringValue = Language.getText( textKey ); } @Override public String toString() { return stringValue; }; } /** * Trend type. * @author Andras Belicza */ public static enum TrendType { HOURLY ( "module.multiRepAnal.tab.player.tab.trends.hourly" ), DAILY ( "module.multiRepAnal.tab.player.tab.trends.daily" ), MONTHLY( "module.multiRepAnal.tab.player.tab.trends.monthly" ); /** Cache of the string value. */ public final String stringValue; /** Key stroke for fast accessing this chart type. */ public final KeyStroke keyStroke; /** Labels of the different values of the trend. */ public final String[] labels; /** * Creates a new ChartType. * @param textKey key of the text representation * @param yAxisLabelKey key of Y axis label of this chart type */ private TrendType( final String textKey ) { stringValue = Language.getText( textKey ); keyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_1 + ordinal(), InputEvent.CTRL_MASK ); // CTRL+1 for chart type 1, CTRL+2 select chart type 2 etc. if ( "module.multiRepAnal.tab.player.tab.trends.hourly".equals( textKey ) ) { labels = new String[ 24 ]; for ( int i = 0; i < 24; i++ ) labels[ i ] = Integer.toString( i ); } else if ( "module.multiRepAnal.tab.player.tab.trends.daily".equals( textKey ) ) { labels = new String[ DAY_TEXT_KEYS.length ]; for ( int i = 0; i < DAY_TEXT_KEYS.length; i++ ) labels[ i ] = Language.getText( DAY_TEXT_KEYS[ i ] ); } else if ( "module.multiRepAnal.tab.player.tab.trends.monthly".equals( textKey ) ) { labels = new String[ MONTH_TEXT_KEYS.length ]; for ( int i = 0; i < MONTH_TEXT_KEYS.length; i++ ) labels[ i ] = Language.getText( MONTH_TEXT_KEYS[ i ] ); } else throw new RuntimeException( "Set the labels for the trend: " + stringValue ); } @Override public String toString() { return stringValue; }; } /** Header keys of the players table. */ private static final String[] PLAYERS_HEADER_KEYS = new String[] { "module.multiRepAnal.tab.players.header.playerName", "module.multiRepAnal.tab.players.header.replays", "module.multiRepAnal.tab.players.header.avgApm", "module.multiRepAnal.tab.players.header.avgEapm", "module.multiRepAnal.tab.players.header.avgApmRedundancy", "module.multiRepAnal.tab.players.header.record", "module.multiRepAnal.tab.players.header.winRatio", "module.multiRepAnal.tab.players.header.raceDistribution", "module.multiRepAnal.tab.players.header.totalTimeInGames", "module.multiRepAnal.tab.players.header.avgGameLength", "module.multiRepAnal.tab.players.header.presence", "module.multiRepAnal.tab.players.header.avgGamesPerDay", "module.multiRepAnal.tab.players.header.firstGame", "module.multiRepAnal.tab.players.header.lastGame" }; /** Header keys of the player maps table. */ private static final String[] PLAYER_MAPS_HEADER_KEYS = new String[] { "module.multiRepAnal.tab.maps.header.mapName", "module.multiRepAnal.tab.maps.header.replays", "module.multiRepAnal.tab.maps.header.replaysRatio", "module.multiRepAnal.tab.maps.header.record", "module.multiRepAnal.tab.maps.header.winRatio", "module.multiRepAnal.tab.maps.header.1v1PWinRatio", "module.multiRepAnal.tab.maps.header.1v1TWinRatio", "module.multiRepAnal.tab.maps.header.1v1ZWinRatio", "module.multiRepAnal.tab.maps.header.avgGameLength", "module.multiRepAnal.tab.maps.header.firstPlayed", "module.multiRepAnal.tab.maps.header.lastPlayed" }; /** Header keys of the 1v1 build orders table. */ private static final String[] BUILD_ORDERS_1V1_HEADER_KEYS = new String[] { "module.multiRepAnal.tab.1v1BuildOrders.header.race", "module.multiRepAnal.tab.1v1BuildOrders.header.buildOrder", "module.multiRepAnal.tab.1v1BuildOrders.header.occurrences", "module.multiRepAnal.tab.1v1BuildOrders.header.record", "module.multiRepAnal.tab.1v1BuildOrders.header.winRatio", "module.multiRepAnal.tab.1v1BuildOrders.header.recordVsP", "module.multiRepAnal.tab.1v1BuildOrders.header.recordVsT", "module.multiRepAnal.tab.1v1BuildOrders.header.recordVsZ", "module.multiRepAnal.tab.1v1BuildOrders.header.firstUsed", "module.multiRepAnal.tab.1v1BuildOrders.header.lastUsed" }; /** Header keys of the Chat words table. */ private static final String[] CHAT_WORDS_HEADER_KEYS = new String[] { "module.multiRepAnal.tab.chatWords.header.word", "module.multiRepAnal.tab.1v1BuildOrders.header.occurrences", "module.multiRepAnal.tab.maps.header.replays", "module.multiRepAnal.tab.maps.header.replaysRatio", "module.multiRepAnal.tab.1v1BuildOrders.header.firstUsed", "module.multiRepAnal.tab.1v1BuildOrders.header.lastUsed" }; /** Header keys of the playmates table. */ private static final String[] PLAYMATES_HEADER_KEYS = new String[] { "module.multiRepAnal.tab.player.tab.playmates.header.playmate", "module.multiRepAnal.tab.player.tab.playmates.header.commonGames", "module.multiRepAnal.tab.player.tab.playmates.header.gamesAsAllies", "module.multiRepAnal.tab.player.tab.playmates.header.gamesAsOpponents", "module.multiRepAnal.tab.player.tab.playmates.header.recordAsAllies", "module.multiRepAnal.tab.player.tab.playmates.header.winRatioAsAllies", "module.multiRepAnal.tab.player.tab.playmates.header.recordAsOpponents", "module.multiRepAnal.tab.player.tab.playmates.header.winRatioAsOpponents", "module.multiRepAnal.tab.player.tab.playmates.header.totalTimeTogether", "module.multiRepAnal.tab.player.tab.playmates.header.firstCommonGame", "module.multiRepAnal.tab.player.tab.playmates.header.lastCommonGame" }; /** Header names of the players table. */ private static final Vector< String > PLAYERS_HEADER_NAME_VECTOR = new Vector< String >( PLAYERS_HEADER_KEYS .length ); /** Header names of the maps table. */ private static final Vector< String > MAPS_HEADER_NAME_VECTOR = new Vector< String >( PLAYER_MAPS_HEADER_KEYS .length-2 ); // 2 columns are not in the general maps table /** Header names of the 1v1 build orders table. */ private static final Vector< String > BUILD_ORDERS_1V1_HEADER_NAME_VECTOR = new Vector< String >( BUILD_ORDERS_1V1_HEADER_KEYS.length ); /** Header names of the non 1v1 build orders table. */ private static final Vector< String > BUILD_ORDERS_NON_1V1_HEADER_NAME_VECTOR = new Vector< String >( BUILD_ORDERS_1V1_HEADER_KEYS.length-3 ); // 3 columns are not in the non 1v1 build orders table /** Header names of the Chat words table. */ private static final Vector< String > CHAT_WORDS_HEADER_NAME_VECTOR = new Vector< String >( CHAT_WORDS_HEADER_KEYS .length ); /** Header names of the type records table. */ private static final Vector< String > TYPE_RECORDS_HEADER_NAME_VECTOR; /** Header names of the format records table. */ private static final Vector< String > FORMAT_RECORDS_HEADER_NAME_VECTOR; /** Header names of the league match-up records table. */ private static final Vector< String > LEAGUE_MATCHUP_RECORDS_HEADER_NAME_VECTOR; /** Header names of the match-up records table. */ private static final Vector< String > MATCHUP_RECORDS_HEADER_NAME_VECTOR; /** Header names of the match-up by maps records table. */ private static final Vector< String > MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR; /** Header names of the game length records table. */ private static final Vector< String > GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR; /** Header names of the day records table. */ private static final Vector< String > DAY_RECORDS_HEADER_NAME_VECTOR; /** Header names of the hour records table. */ private static final Vector< String > HOUR_RECORDS_HEADER_NAME_VECTOR; /** Header names of the player maps table. */ private static final Vector< String > PLAYER_MAPS_HEADER_NAME_VECTOR = new Vector< String >( PLAYER_MAPS_HEADER_KEYS .length ); /** Header names of the playmates table. */ private static final Vector< String > PLAYMATES_HEADER_NAME_VECTOR = new Vector< String >( PLAYMATES_HEADER_KEYS .length ); /** Header names of the gaming sessions table. */ private static final Vector< String > GAMING_SESSIONS_HEADER_NAME_VECTOR; static { for ( int i = 0; i < PLAYERS_HEADER_KEYS.length; i++ ) PLAYERS_HEADER_NAME_VECTOR.add( Language.getText( PLAYERS_HEADER_KEYS[ i ] ) ); for ( int i = 0; i < BUILD_ORDERS_1V1_HEADER_KEYS.length; i++ ) BUILD_ORDERS_1V1_HEADER_NAME_VECTOR.add( Language.getText( BUILD_ORDERS_1V1_HEADER_KEYS[ i ] ) ); for ( int i = 0; i < BUILD_ORDERS_1V1_HEADER_NAME_VECTOR.size(); i++ ) if ( !"module.multiRepAnal.tab.1v1BuildOrders.header.recordVsP".equals( BUILD_ORDERS_1V1_HEADER_KEYS[ i ] ) && !"module.multiRepAnal.tab.1v1BuildOrders.header.recordVsT".equals( BUILD_ORDERS_1V1_HEADER_KEYS[ i ] ) && !"module.multiRepAnal.tab.1v1BuildOrders.header.recordVsZ".equals( BUILD_ORDERS_1V1_HEADER_KEYS[ i ] ) ) BUILD_ORDERS_NON_1V1_HEADER_NAME_VECTOR.add( BUILD_ORDERS_1V1_HEADER_NAME_VECTOR.get( i ) ); for ( int i = 0; i < CHAT_WORDS_HEADER_KEYS.length; i++ ) CHAT_WORDS_HEADER_NAME_VECTOR.add( Language.getText( CHAT_WORDS_HEADER_KEYS[ i ] ) ); @SuppressWarnings("unchecked") final Vector< String > TYPE_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) PLAYERS_HEADER_NAME_VECTOR.clone(); // To suppress "unchecked" warning TYPE_RECORDS_HEADER_NAME_VECTOR = TYPE_RECORDS_HEADER_NAME_VECTOR_; TYPE_RECORDS_HEADER_NAME_VECTOR.set( 0, Language.getText( "module.multiRepAnal.tab.player.tab.typeRecords.header.type" ) ); @SuppressWarnings("unchecked") final Vector< String > FORMAT_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) PLAYERS_HEADER_NAME_VECTOR.clone(); FORMAT_RECORDS_HEADER_NAME_VECTOR = FORMAT_RECORDS_HEADER_NAME_VECTOR_; FORMAT_RECORDS_HEADER_NAME_VECTOR.set( 0, Language.getText( "module.multiRepAnal.tab.player.tab.formatRecords.header.format" ) ); @SuppressWarnings("unchecked") final Vector< String > GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) PLAYERS_HEADER_NAME_VECTOR.clone(); GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR = GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR_; GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR.set( 0, Language.getText( "module.multiRepAnal.tab.player.tab.gameLengthRecords.header.gameLength" ) ); @SuppressWarnings("unchecked") final Vector< String > DAY_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) PLAYERS_HEADER_NAME_VECTOR.clone(); DAY_RECORDS_HEADER_NAME_VECTOR = DAY_RECORDS_HEADER_NAME_VECTOR_; DAY_RECORDS_HEADER_NAME_VECTOR.set( 0, Language.getText( "module.multiRepAnal.tab.player.tab.dayRecords.header.day" ) ); @SuppressWarnings("unchecked") final Vector< String > HOUR_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) PLAYERS_HEADER_NAME_VECTOR.clone(); HOUR_RECORDS_HEADER_NAME_VECTOR = HOUR_RECORDS_HEADER_NAME_VECTOR_; HOUR_RECORDS_HEADER_NAME_VECTOR.set( 0, Language.getText( "module.multiRepAnal.tab.player.tab.hourRecords.header.hour" ) ); LEAGUE_MATCHUP_RECORDS_HEADER_NAME_VECTOR = new Vector<>( PLAYERS_HEADER_KEYS.length ); LEAGUE_MATCHUP_RECORDS_HEADER_NAME_VECTOR.add( Language.getText( "module.multiRepAnal.tab.player.tab.leagueMatchupRecords.header.leagues" ) ); for ( int i = 1; i < PLAYERS_HEADER_NAME_VECTOR.size(); i++ ) LEAGUE_MATCHUP_RECORDS_HEADER_NAME_VECTOR.add( Language.getText( PLAYERS_HEADER_KEYS[ i ] ) ); MATCHUP_RECORDS_HEADER_NAME_VECTOR = new Vector<>( PLAYERS_HEADER_KEYS.length - 1 ); // The Races % column is not shown because each line is for 1 race (would always be "X:100%" MATCHUP_RECORDS_HEADER_NAME_VECTOR.add( Language.getText( "module.multiRepAnal.tab.player.tab.matchupRecords.header.matchup" ) ); for ( int i = 1; i < PLAYERS_HEADER_NAME_VECTOR.size(); i++ ) if ( !"module.multiRepAnal.tab.players.header.raceDistribution".equals( PLAYERS_HEADER_KEYS[ i ] ) ) MATCHUP_RECORDS_HEADER_NAME_VECTOR.add( Language.getText( PLAYERS_HEADER_KEYS[ i ] ) ); for ( int i = 0; i < PLAYER_MAPS_HEADER_KEYS.length; i++ ) PLAYER_MAPS_HEADER_NAME_VECTOR.add( Language.getText( PLAYER_MAPS_HEADER_KEYS[ i ] ) ); @SuppressWarnings("unchecked") final Vector< String > MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR_ = (Vector< String >) MATCHUP_RECORDS_HEADER_NAME_VECTOR.clone(); MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR = MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR_; MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR.add( 0, Language.getText( "module.multiRepAnal.tab.maps.header.mapName" ) ); // 2 columns are not in the general maps table for ( int i = 0; i < PLAYER_MAPS_HEADER_NAME_VECTOR.size(); i++ ) if ( !"module.multiRepAnal.tab.maps.header.record".equals( PLAYER_MAPS_HEADER_KEYS[ i ] ) && !"module.multiRepAnal.tab.maps.header.winRatio".equals( PLAYER_MAPS_HEADER_KEYS[ i ] ) ) MAPS_HEADER_NAME_VECTOR.add( PLAYER_MAPS_HEADER_NAME_VECTOR.get( i ) ); for ( int i = 0; i < PLAYMATES_HEADER_KEYS.length; i++ ) PLAYMATES_HEADER_NAME_VECTOR.add( Language.getText( PLAYMATES_HEADER_KEYS[ i ] ) ); GAMING_SESSIONS_HEADER_NAME_VECTOR = new Vector<>( PLAYERS_HEADER_NAME_VECTOR.size() + 1 ); GAMING_SESSIONS_HEADER_NAME_VECTOR.add( 0, Language.getText( "module.multiRepAnal.tab.player.tab.gamingSessions.header.gameInSessions" ) ); GAMING_SESSIONS_HEADER_NAME_VECTOR.addAll( PLAYERS_HEADER_NAME_VECTOR ); GAMING_SESSIONS_HEADER_NAME_VECTOR.set( 1, Language.getText( "module.multiRepAnal.tab.player.tab.gamingSessions.header.sessionsEndingHere" ) ); } /** Simple counter. */ private static final AtomicInteger counter = new AtomicInteger(); /** Files to be analyzed. */ private final File[] files; /** Check box to tell if the first player should be auto-opened. */ private final JCheckBox autoOpenFirstPlayerCheckBox = GuiUtils.createCheckBox( "module.multiRepAnal.autoOpenFirstPlayer", Settings.KEY_MULTI_REP_ANAL_AUTO_OPEN_FIRST_PLAYER ); /** Check box to tell if tables have to be stretched to window. */ private final JCheckBox stretchToWindowCheckBox = GuiUtils.createCheckBox( "module.multiRepAnal.stretchToWindow", Settings.KEY_MULTI_REP_ANAL_STRETCH_TO_WINDOW ); /** Reference to the tabbed pane of this internal frame. */ private final JTabbedPane tabbedPane = new JTabbedPane(); /** Number of replays that are included in the analysis. */ private int replaysIncludedInAnalysis; /** The setting that tells whether to use real time. */ private final boolean useRealTime = Settings.getBoolean( Settings.KEY_SETTINGS_MISC_USE_REAL_TIME_MEASUREMENT ); /** Map of player statistics. Maps from player name (full name) to the statistics. */ private final Map< String, PlayerStatistics > playerStatisticsMap = new HashMap< String, PlayerStatistics >(); /** Map of map statistics. Maps from map name to the statistics. */ private final Map< String, MapStatistics > mapStatisticsMap = new HashMap< String, MapStatistics >(); /** Build order statistics for all formats. Value maps from build order to the statistics. */ private final Map< Format, Map< String, BuildOrderStatistics > > formatBuildOrderStatisticsMap = new EnumMap< Format, Map< String, BuildOrderStatistics > >( Format.class ); /** Map of words statistics. Maps from word to the statistics. */ private final Map< String, WordStatistics > chatWordsStatisticsMap = new HashMap< String, WordStatistics >(); /** Name of the first player (with the most games). */ private String firstPlayerName; /** Mouse listener for player tables which handles opening player for double click, and shows the player menu for right click. */ private final MouseListener playersTableMouseListener = new MouseAdapter() { @Override public void mouseClicked( final MouseEvent event ) { final JTable table = (JTable) event.getSource(); if ( event.getButton() == GuiUtils.MOUSE_BUTTON_LEFT ) { if ( table.getSelectedRow() >= 0 && event.getClickCount() == 2 ) { final String playerName = (String) table.getValueAt( table.rowAtPoint( event.getPoint() ), table.convertColumnIndexToView( 0 ) ); openPlayer( playerName ); } } else if ( event.getButton() == GuiUtils.MOUSE_BUTTON_RIGHT ) { final int row = table.rowAtPoint( event.getPoint() ); table.getSelectionModel().setSelectionInterval( row, row ); // Select only 1 player final String playerName = (String) table.getValueAt( row, table.convertColumnIndexToView( 0 ) ); final PlayerStatistics ps = playerStatisticsMap.get( playerName ); if ( ps != null ) { // It can be null (computer playmates for example) final JPopupMenu playerPopupMenu = new PlayerPopupMenu( ps.playerId, ps.playerType ); playerPopupMenu.addSeparator(); final JMenuItem openPlayerMenuItem = new JMenuItem( Language.getText( "module.multiRepAnal.playerMenu.openPlayer" ), Icons.CHART_UP ); openPlayerMenuItem.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { openPlayer( playerName ); } } ); playerPopupMenu.add( openPlayerMenuItem ); playerPopupMenu.show( event.getComponent(), event.getX(), event.getY() ); } } } }; /** * Creates a new MultiRepAnalysis * @param arguments optional arguments to define the files and folders to analyze<br> * the <b>first</b> element can be an optional replay source to load<br> * the <b>second</b> element can be an optional replay list to load<br> * the <b>third</b> element can be a File array to perform the Multi-rep analysis on */ public MultiRepAnalysis( final Object... arguments ) { super( arguments.length == 0 ? Language.getText( "module.multiRepAnal.opening" ) : null ); // This title does not have a role as this internal frame is not displayed until replays are chosen, and then title is changed anyway setFrameIcon( Icons.CHART_UP_COLOR ); if ( arguments.length == 0 ) { final JFileChooser fileChooser = new JFileChooser( GeneralUtils.getDefaultReplayFolder() ); fileChooser.setDialogTitle( Language.getText( "module.multiRepAnal.openTitle" ) ); fileChooser.setFileFilter( GuiUtils.SC2_REPLAY_FILTER ); fileChooser.setAccessory( GuiUtils.createReplayFilePreviewAccessory( fileChooser ) ); fileChooser.setFileView( GuiUtils.SC2GEARS_FILE_VIEW ); fileChooser.setFileSelectionMode( JFileChooser.FILES_AND_DIRECTORIES ); fileChooser.setMultiSelectionEnabled( true ); if ( fileChooser.showOpenDialog( MainFrame.INSTANCE ) == JFileChooser.APPROVE_OPTION ) this.files = fileChooser.getSelectedFiles(); else { dispose(); this.files = null; return; } } else { if ( arguments.length > 0 && arguments[ 0 ] != null ) { // Replay source this.files = loadReplaySourceFile( (File) arguments[ 0 ] ); } else if ( arguments.length > 1 && arguments[ 1 ] != null ) { // Replay list // TODO this can be sped up by reading the replay list by hand and only use the file name! final List< Object[] > dataList = ReplaySearch.loadReplayListFile( (File) arguments[ 1 ] ); this.files = new File[ dataList.size() ]; for ( int i = dataList.size() - 1; i >= 0; i-- ) this.files[ i ] = new File( (String) dataList.get( i )[ ReplaySearch.COLUMN_FILE_NAME ] ); } else if ( arguments.length > 2 && arguments[ 2 ] != null ) { // Replays to open this.files = (File[]) arguments[ 2 ]; } else throw new RuntimeException( "The source for Multi-rep analysis is incorrectly specified!" ); } setTitle( Language.getText( "module.multiRepAnal.title", counter.incrementAndGet() ) ); buildGUI(); } /** * Loads the specified replay source file. * @param replaySource replay source file to be loaded * @return the files denoted by the replay source */ private static File[] loadReplaySourceFile( final File replaySource ) { try ( final BufferedReader input = new BufferedReader( new InputStreamReader( new FileInputStream( replaySource ), Consts.UTF8 ) ) ) { final List< File > replayList = new ArrayList< File >(); while ( input.ready() ) replayList.add( new File( input.readLine() ) ); return replayList.toArray( new File[ replayList.size() ] ); } catch ( final Exception e ) { e.printStackTrace(); GuiUtils.showErrorDialog( Language.getText( "module.repSearch.tab.source.failedToLoadRepSource" ) ); return new File[ 0 ]; } } /** * Builds the GUI of the frame. */ private void buildGUI() { final Box northBox = Box.createVerticalBox(); final Box buttonsBox = Box.createHorizontalBox(); final JButton abortButton = new JButton( Icons.CROSS_OCTAGON ); GuiUtils.updateButtonText( abortButton, "module.multiRepAnal.abortAnalysisButton" ); buttonsBox.add( abortButton ); autoOpenFirstPlayerCheckBox.setToolTipText( Language.getText( "module.multiRepAnal.autoOpenFirstPlayerToolTip" ) ); buttonsBox.add( autoOpenFirstPlayerCheckBox ); buttonsBox.add( Box.createHorizontalStrut( 5 ) ); buttonsBox.add( stretchToWindowCheckBox ); buttonsBox.add( Box.createHorizontalStrut( 15 ) ); buttonsBox.add( MiscSettingsDialog.createLinkLabelToSettings( SettingsTab.MULTI_REP_ANALYSIS ) ); buttonsBox.add( Box.createHorizontalStrut( 15 ) ); buttonsBox.add( MiscSettingsDialog.createLinkLabelToSettings( SettingsTab.ALIASES ) ); northBox.add( GuiUtils.wrapInPanel( buttonsBox ) ); // If not wrapped, it doesn't get center-aligned final JProgressBar progressBar = new JProgressBar(); northBox.add( progressBar ); getContentPane().add( northBox, BorderLayout.NORTH ); getContentPane().add( tabbedPane, BorderLayout.CENTER ); final Thread analysisThread = new NormalThread( "Multi-replay analysis" ) { private volatile boolean aborted; private final ExecutorService executorService = GeneralUtils.createMultiThreadedExecutorService(); private int replaysCount; private volatile int analyzedCount; private volatile int skippedCount; // We have to use the same settings for all replays private final int timeLimitToBeIncluded = Settings.getInt ( Settings.KEY_SETTINGS_MISC_TIME_LIMIT_FOR_MULTI_REP_ANALYSIS ); private final int buildOrderLength = Settings.getInt ( Settings.KEY_SETTINGS_MISC_BUILD_ORDER_LENGTH ); private final int timeExclusionForApm = Settings.getInt ( Settings.KEY_SETTINGS_MISC_INITIAL_TIME_TO_EXCLUDE_FROM_APM ) << FRAME_BITS_IN_SECOND; private final boolean cacheEnabled = Settings.getBoolean( Settings.KEY_SETTINGS_MISC_CACHE_PREPROCESSED_REPLAYS ); @Override public void run() { final InternalFrameListener abortListener = new InternalFrameAdapter() { @Override public void internalFrameClosing( final InternalFrameEvent event ) { // If the Multi-replay analysis is closed, we want to stop the analysis abortButton.doClick(); } }; addInternalFrameListener( abortListener ); abortButton.addActionListener( new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { buttonsBox.remove( abortButton ); getContentPane().validate(); aborted = true; } } ); progressBar.setStringPainted( true ); progressBar.setString( Language.getText( "module.multiRepAnal.countingReplays" ) ); for ( final File file : files ) { if ( aborted ) break; replaysCount += countReplays( file ); } if ( aborted ) { GeneralUtils.shutdownExecutorService( executorService ); progressBar.setString( Language.getText( "module.multiRepAnal.analysisAborted" ) + " [" + progressBar.getString() + "]" ); } else { progressBar.setMaximum( replaysCount ); updateProgressBar(); for ( final File file : files ) { if ( aborted ) break; analyzeReplay( file ); } GeneralUtils.shutdownExecutorService( executorService ); if ( aborted ) { progressBar.setString( Language.getText( "module.multiRepAnal.analysisAborted" ) + " [" + progressBar.getString() + "]" ); } else { buttonsBox.remove( abortButton ); getContentPane().validate(); } replaysIncludedInAnalysis = analyzedCount - skippedCount; // Now we have statistical info, build the rest of the GUI // Swing is single-threaded. We cannot update the GUI directly, else the Nimbus LaF thows ClassCastExceptions inconsistently. // (java.lang.ClassCastException: javax.swing.plaf.BorderUIResource cannot be cast to java.awt.Font) try { SwingUtilities.invokeAndWait( new Runnable() { @Override public void run() { GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.players.title" ), Icons.USERS, false, tabbedPane, createPlayersTab(), false, null ); final JTabbedPane globalStatsTabbedPane = new JTabbedPane(); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.maps.title" ), Icons.MAPS_STACK, false, globalStatsTabbedPane, createMapsTab (), false, null ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.buildOrders.title" ), Icons.BLOCK , false, globalStatsTabbedPane, createBuildOrdersTab(), false, null ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.chatWords.title" ), Icons.BALLOONS , false, globalStatsTabbedPane, createChatWordsTab (), false, null ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.globalStats.title" ), Icons.SUM, false, tabbedPane, globalStatsTabbedPane, false, null ); // Auto-open first player if it's required if ( autoOpenFirstPlayerCheckBox.isSelected() && firstPlayerName != null ) openPlayer( firstPlayerName ); } } ); } catch ( final InterruptedException ie ) { ie.printStackTrace(); } catch ( final InvocationTargetException ite ) { ite.printStackTrace(); } } removeInternalFrameListener( abortListener ); } private int countReplays( final File file ) { if ( file.isFile() ) return GuiUtils.SC2_REPLAY_FILTER.accept( file ) ? 1 : 0; else { final File[] children = file.listFiles(); int replaysCount = 0; if ( children != null ) for ( final File child : children ) { if ( aborted ) return 0; replaysCount += countReplays( child ); } return replaysCount; } } /** This is here to distinguish different players with the same name.<br> * If only one occurrence found for a name, the value is the player identifier.<br> * If more occurrences were found, the value will be a list of player identifiers.*/ private final Map< String, Object > playerNameClonesMap = new HashMap< String, Object >(); /** Lock to be used not to use same instance variables concurrently. */ private final Object multiThreadLock = playerNameClonesMap; private void analyzeReplay( final File file ) { if ( file.isFile() ) { if ( GuiUtils.SC2_REPLAY_FILTER.accept( file ) ) // TODO: this will exclude explicitly specified files having different extension! executorService.execute( new Runnable() { @Override public void run() { if ( aborted ) // ExecutorService.shutdown() continues to execute queued tasks, so check if aborted return; analyzedCount++; final Replay replay = ReplayCache.getReplay( file, buildOrderLength, timeExclusionForApm, cacheEnabled, cacheEnabled, null ); if ( replay != null && replay.gameLength >= ( timeLimitToBeIncluded << 1 ) ) // To exclude short games synchronized ( multiThreadLock ) { final Date replayDate = new Date( replay.details.saveTime ); final Date startDate = new Date( replay.details.saveTime - 1000L*replay.initData.gameSpeed.convertToRealTime( replay.gameLengthSec ) ); final Player[] players = replay.details.players; final Format format = replay.initData.format; // Distinguish different players with same name, apply aliases final String[] playerDisplayNames = new String[ players.length ]; for ( int playerIndex = 0; playerIndex < players.length; playerIndex++ ) { final Player player = players[ playerIndex ]; // Apply player aliases player.playerId = Settings.getAliasGroupPlayerId( player.playerId ); final PlayerId playerId = player.playerId; int cloneId = -1; final Object clones = playerNameClonesMap.get( playerId.name ); if ( clones == null ) { // First occurrence playerNameClonesMap.put( player.playerId.name, playerId ); cloneId = 0; } else { if ( clones instanceof List< ? > ) { // At least 2 occurrence before @SuppressWarnings("unchecked") final List< PlayerId > cloneList = (List< PlayerId >) clones; for ( int i = 0; i < cloneList.size(); i++ ) if ( playerId.equals( cloneList.get( i ) ) ) { cloneId = i; break; } if ( cloneId < 0 ) { // New occurrence cloneId = cloneList.size(); cloneList.add( playerId ); } } else { if ( playerId.equals( (PlayerId) clones ) ) cloneId = 0; // The same occurrence as before else { // 2nd occurrence final List< PlayerId > cloneList = new ArrayList< PlayerId >( 2 ); cloneList.add( (PlayerId) clones ); cloneList.add( playerId ); cloneId = 1; playerNameClonesMap.put( playerId.name, cloneList ); } } } playerDisplayNames[ playerIndex ] = cloneId == 0 ? playerId.name : playerId.name + " (#" + (cloneId+1) + ")"; } final Map< String, IntHolder >[] wordCountMaps = replay.messageEvents.getWordCountMaps( replay.initData.clientNames == null ? players.length : replay.initData.clientNames.length ); // Player statistics Race winner1v1Race = null; Race loser1v1Race = null; final PlayerGameParticipationStats[] pgpss = new PlayerGameParticipationStats[ players.length ]; final Set< String > replayWordSet = new HashSet< String >(); for ( int playerIndex = 0; playerIndex < players.length; playerIndex++ ) { final Player player = players[ playerIndex ]; int secondsInGame = player.lastActionFrame >> FRAME_BITS_IN_SECOND; if ( useRealTime ) secondsInGame = replay.initData.gameSpeed.convertToRealTime( secondsInGame ); if ( secondsInGame < timeLimitToBeIncluded ) continue; int secondsInGameForApm = Math.max( 0, player.lastActionFrame - replay.excludedInitialFrames ) >> FRAME_BITS_IN_SECOND; if ( useRealTime ) secondsInGameForApm = replay.initData.gameSpeed.convertToRealTime( secondsInGameForApm ); int totalInjectionGap = player.totalInjectionGap; if ( useRealTime ) totalInjectionGap = replay.initData.gameSpeed.convertToRealTime( totalInjectionGap ); PlayerStatistics ps = playerStatisticsMap.get( playerDisplayNames[ playerIndex ] ); if ( ps == null ) playerStatisticsMap.put( playerDisplayNames[ playerIndex ], ps = new PlayerStatistics( playerDisplayNames[ playerIndex ], player ) ); pgpss[ playerIndex ] = new PlayerGameParticipationStats( replayDate, startDate, format, secondsInGame, secondsInGameForApm, player, players.length > 1 ? players[ ( players[ 0 ] == player ? 1 : 0 ) ].finalRace : Race.UNKNOWN, replay.details.mapName, replay.initData.gameSpeed, getTeamLeagueCompositions( replay.details, player, format ), getTeamRaceCompositions ( replay.details, player, format ), replay.initData.gameType, totalInjectionGap, wordCountMaps[ playerIndex ] ); ps.buildInPlayerGameParticipation( pgpss[ playerIndex ] ); // This is for the map statistics: if ( format == Format.ONE_VS_ONE ) if ( player.isWinner != null ) if ( player.isWinner ) winner1v1Race = player.finalRace; else loser1v1Race = player.finalRace; for ( int playMateIndex = 0; playMateIndex < players.length; playMateIndex++ ) if ( playMateIndex != playerIndex ) ( player.team != Player.TEAM_UNKNOWN && player.team == players[ playMateIndex ].team ? pgpss[ playerIndex ].allyList : pgpss[ playerIndex ].opponentList ).add( playerDisplayNames[ playMateIndex ] ); ps.playerGameParticipationStatsList.add( pgpss[ playerIndex ] ); } // Map statistics MapStatistics ms = mapStatisticsMap.get( replay.details.mapName ); if ( ms == null ) mapStatisticsMap.put( replay.details.mapName, ms = new MapStatistics( replay.details.mapName ) ); ms.record.totalGames++; ms.totalTimeSecInGames += useRealTime ? replay.initData.gameSpeed.convertToRealTime( replay.gameLengthSec ) : replay.gameLengthSec; ms.registerDate( replayDate ); if ( winner1v1Race != null ) { final Record record = ms.getRaceRecord( winner1v1Race ); record.totalGames++; record.wins++; } if ( loser1v1Race != null ) { final Record record = ms.getRaceRecord( loser1v1Race ); record.totalGames++; record.losses++; } // Build order statistics final int[] counters = new int[ players.length ]; int doneCounter = 0; for ( int i = 0; i < pgpss.length; i++ ) if ( pgpss[ i ] == null ) { // Player left before 2 minutes, do not parse build orders for him/her counters[ i ] = buildOrderLength; doneCounter++; } final Building[][] buildingss = new Building[ players.length ][ buildOrderLength ]; final Action[] actions = replay.gameEvents.actions; final int actionsLength = actions.length; Action action; for ( int i = 0; i < actionsLength; i++ ) { if ( ( action = actions[ i ] ) instanceof BuildAction && counters[ action.player ] < buildOrderLength ) { buildingss[ action.player ][ counters[ action.player ] ] = ( (BuildAction) action ).building; if ( ++counters[ action.player ] == buildOrderLength ) if ( ++doneCounter == counters.length ) break; } } for ( int i = buildingss.length - 1; i >= 0; i-- ) { if ( pgpss[ i ] == null ) continue; final Building[] buildings = buildingss[ i ]; final int buildingsLength = counters[ i ]; if ( buildingsLength == 0 ) continue; final StringBuilder builder = new StringBuilder(); for ( int j = 0; j < buildingsLength; j++ ) { if ( builder.length() > 0 ) builder.append( ", " ); builder.append( buildings[ j ].stringValue ); } final String buildOrder = builder.toString(); pgpss[ i ].buildOrder = buildOrder; Map< String, BuildOrderStatistics > buildOrderStatisticsMap = formatBuildOrderStatisticsMap.get( replay.initData.format ); if ( buildOrderStatisticsMap == null ) formatBuildOrderStatisticsMap.put( replay.initData.format, buildOrderStatisticsMap = new HashMap< String, BuildOrderStatistics >() ); BuildOrderStatistics bs = buildOrderStatisticsMap.get( buildOrder ); if ( bs == null ) buildOrderStatisticsMap.put( buildOrder, bs = new BuildOrderStatistics( buildOrder, pgpss[ i ].race, replay.initData.format ) ); bs.buildInPlayerGameParticipation( pgpss[ i ] ); } // Chat words statistics for ( final Map< String, IntHolder > wordCountMap : wordCountMaps ) { if ( wordCountMap != null ) for ( final Entry< String, IntHolder > entry : wordCountMap.entrySet() ) { final String word = entry.getKey(); WordStatistics wordStatistics = chatWordsStatisticsMap.get( word ); if ( wordStatistics == null ) chatWordsStatisticsMap.put( word, wordStatistics = new WordStatistics( entry.getKey() ) ); wordStatistics.registerDate( replayDate ); wordStatistics.count += entry.getValue().value; if ( replayWordSet.add( word ) ) wordStatistics.replays++; } } } else skippedCount++; updateProgressBar(); } } ); } else { final File[] children = file.listFiles(); if ( children != null ) for ( final File child : children ) { if ( aborted ) return; analyzeReplay( child ); } } } // Re-used objects... final List< String > helperPlayerList = Arrays.asList( "" ); final List< Character > letterList = new ArrayList< Character >( 4 ); final StringBuilder teamCompositionBuilder = new StringBuilder( 5 ); private String[] getTeamLeagueCompositions( final Details details, final Player player, final Format format ) { if ( details.players.length == 0 || details.players[ 0 ].getLeague() == League.UNKNOWN ) // League is only available from replay version 2.0. return new String[ 0 ]; if ( format == Format.ONE_VS_ONE ) return new String[] { Character.toString( player.getLeague().letter ), Character.toString( ( details.players.length > 1 ? details.players[ ( details.players[ 0 ] == player ? 1 : 0 ) ].getLeague() : League.UNKNOWN ).letter ) }; helperPlayerList.set( 0, player.playerId.name ); details.rearrangePlayers( helperPlayerList ); final int[] teamOrderPlayerIndices = details.getTeamOrderPlayerIndices(); letterList.clear(); final String[] teamCompositions; if ( format == Format.FREE_FOR_ALL ) { teamCompositions = new String[ teamOrderPlayerIndices.length ]; // 1 player in each team, many teams, so we have to sort the teams to get the race letters sorted for ( int i = 0; i < teamOrderPlayerIndices.length; i++ ) letterList.add( details.players[ teamOrderPlayerIndices[ i ] ].getLeague().letter ); Collections.sort( letterList ); for ( int i = teamOrderPlayerIndices.length - 1; i >= 0; i-- ) teamCompositions[ i ] = Character.toString( letterList.get( i ) ); } else { int teamsCount = 1; int lastTeam = details.players[ teamOrderPlayerIndices[ 0 ] ].team; for ( int i = 1; i < teamOrderPlayerIndices.length; i++ ) { final int team = details.players[ teamOrderPlayerIndices[ i ] ].team; if ( team != lastTeam ) { lastTeam = team; teamsCount++; } } teamCompositions = new String[ teamsCount ]; teamCompositionBuilder.setLength( 0 ); teamCompositionBuilder.append( details.players[ teamOrderPlayerIndices[ 0 ] ].getLeague().letter ); teamCompositionBuilder.append( '+' ); teamsCount = 0; lastTeam = details.players[ teamOrderPlayerIndices[ 0 ] ].team; for ( int i = 1; i < teamOrderPlayerIndices.length; i++ ) { final int team = details.players[ teamOrderPlayerIndices[ i ] ].team; if ( team != lastTeam ) { Collections.sort( letterList ); for ( final Character leagueLetter : letterList ) teamCompositionBuilder.append( leagueLetter ); teamCompositions[ teamsCount++ ] = teamCompositionBuilder.toString(); teamCompositionBuilder.setLength( 0 ); letterList.clear(); lastTeam = team; } letterList.add( details.players[ teamOrderPlayerIndices[ i ] ].getLeague().letter ); } Collections.sort( letterList ); for ( final Character leagueLetter : letterList ) teamCompositionBuilder.append( leagueLetter ); teamCompositions[ teamCompositions.length - 1 ] = teamCompositionBuilder.toString(); } return teamCompositions; } private String[] getTeamRaceCompositions( final Details details, final Player player, final Format format ) { if ( details.players.length == 0 ) return new String[ 0 ]; if ( format == Format.ONE_VS_ONE ) return new String[] { Character.toString( player.finalRace.letter ), Character.toString( ( details.players.length > 1 ? details.players[ ( details.players[ 0 ] == player ? 1 : 0 ) ].finalRace : Race.UNKNOWN ).letter ) }; helperPlayerList.set( 0, player.playerId.name ); details.rearrangePlayers( helperPlayerList ); final int[] teamOrderPlayerIndices = details.getTeamOrderPlayerIndices(); letterList.clear(); final String[] teamCompositions; if ( format == Format.FREE_FOR_ALL ) { teamCompositions = new String[ teamOrderPlayerIndices.length ]; // 1 player in each team, many teams, so we have to sort the teams to get the race letters sorted for ( int i = 0; i < teamOrderPlayerIndices.length; i++ ) letterList.add( details.players[ teamOrderPlayerIndices[ i ] ].getRaceLetter() ); Collections.sort( letterList ); for ( int i = teamOrderPlayerIndices.length - 1; i >= 0; i-- ) teamCompositions[ i ] = Character.toString( letterList.get( i ) ); } else { int teamsCount = 1; int lastTeam = details.players[ teamOrderPlayerIndices[ 0 ] ].team; for ( int i = 1; i < teamOrderPlayerIndices.length; i++ ) { final int team = details.players[ teamOrderPlayerIndices[ i ] ].team; if ( team != lastTeam ) { lastTeam = team; teamsCount++; } } teamCompositions = new String[ teamsCount ]; teamCompositionBuilder.setLength( 0 ); teamCompositionBuilder.append( details.players[ teamOrderPlayerIndices[ 0 ] ].getRaceLetter() ); teamCompositionBuilder.append( '+' ); teamsCount = 0; lastTeam = details.players[ teamOrderPlayerIndices[ 0 ] ].team; for ( int i = 1; i < teamOrderPlayerIndices.length; i++ ) { final int team = details.players[ teamOrderPlayerIndices[ i ] ].team; if ( team != lastTeam ) { Collections.sort( letterList ); for ( final Character raceLetter : letterList ) teamCompositionBuilder.append( raceLetter ); teamCompositions[ teamsCount++ ] = teamCompositionBuilder.toString(); teamCompositionBuilder.setLength( 0 ); letterList.clear(); lastTeam = team; } letterList.add( details.players[ teamOrderPlayerIndices[ i ] ].getRaceLetter() ); } Collections.sort( letterList ); for ( final Character raceLetter : letterList ) teamCompositionBuilder.append( raceLetter ); teamCompositions[ teamCompositions.length - 1 ] = teamCompositionBuilder.toString(); } return teamCompositions; } private void updateProgressBar() { SwingUtilities.invokeLater( new Runnable() { @Override public void run() { progressBar.setValue( analyzedCount ); progressBar.setString( Language.getText( "module.multiRepAnal.analysisStatus", analyzedCount, skippedCount, replaysCount, replaysCount == 0 ? 100 : 100 * analyzedCount / replaysCount ) ); } } ); } }; // We have to start the analysis thread "later", else there some kind of blocking occurs (while this internal frame is active, CTRL+F4 and ALT+F4 are not working (and probably amongst others) ) SwingUtilities.invokeLater( new Runnable() { @Override public void run() { analysisThread.start(); } } ); } /** * Creates and returns the players tab. * @return the players tab */ private JComponent createPlayersTab() { final JPanel panel = new JPanel( new BorderLayout() ); final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( playerStatisticsMap.size() ); for ( final PlayerStatistics ps : playerStatisticsMap.values() ) { final Vector< Object > row = new Vector< Object >( 15 ); row.add( ps.playerDisplayName ); row.add( ps.record.totalGames ); row.add( ps.getAvgApm() ); row.add( ps.getAvgEapm() ); row.add( ps.getAvgApmRedundancy() ); row.add( ps.record ); row.add( ps.record.getWinRatio() ); row.add( ps.getRaceDistributionString() ); row.add( ps.getFormattedTotalTimeInGames() ); row.add( ReplayUtils.formatMs( ps.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( ps.getPresence() ); row.add( ps.getAvgGamesPerDay() ); row.add( Language.formatDate( ps.firstDate ) ); row.add( Language.formatDate( ps.lastDate ) ); row.add( dataVector.add( row ) ); } if ( autoOpenFirstPlayerCheckBox.isSelected() ) { int maxTotalGames = 0; for ( final Vector< Object > row : dataVector ) if ( maxTotalGames < ( (Integer) row.get( 1 ) ).intValue() ) { firstPlayerName = (String) row.get( 0 ); maxTotalGames = ( (Integer) row.get( 1 ) ).intValue(); } } final Holder< JTable > tableHolder = new Holder< JTable >(); createStatisticsTableTab( panel, "module.multiRepAnal.tab.players.info", new Object[] { playerStatisticsMap.size() }, 0, new int[] { 1, 0 }, dataVector, PLAYERS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.players.title" ), 0, 1 ), tableHolder, null ); final JLabel infoLabel = (JLabel) panel.getComponent( 0 ); infoLabel.setOpaque( true ); // Needed for the background color to take effect infoLabel.setBackground( Color.GREEN ); tableHolder.value.addMouseListener( playersTableMouseListener ); registerEnterToOpenPlayer( tableHolder.value ); return panel; } /** * Registers the Enter and Shift+Enter keystrokes to the specified players table to open the selected player. * @param table players table to register keystrokes to */ private void registerEnterToOpenPlayer( final JTable table ) { Object actionKey; // Add Enter and Shift+Enter keystroke to open selected player table.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), actionKey = new Object() ); table.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ), actionKey ); table.getActionMap().put( actionKey, new AbstractAction() { @Override public void actionPerformed( final ActionEvent event ) { if ( table.getSelectedRow() >= 0 ) { final String playerName = (String) table.getValueAt( table.getSelectedRow(), table.convertColumnIndexToView( 0 ) ); openPlayer( playerName ); } } } ); } /** * Opens the detailed statistics of a player. * @param playerName name of the player whose detailed statistics to be opened */ private void openPlayer( final String playerName ) { final PlayerStatistics playerStatistics = playerStatisticsMap.get( playerName ); if ( playerStatistics == null ) // It can be null if the player did not have any "long" games, but was listed as a playmate return; final Holder< JComponent > chartsPanelHolder = new Holder< JComponent >(); // Action listeners to stretch to window have to be removed if player tab is closed. final List< ActionListener > stretchToWindowActionListenerList = new ArrayList< ActionListener >(); GuiUtils.addNewTab( playerName, Icons.USER, true, tabbedPane, createPlayerTab( playerStatistics, chartsPanelHolder, stretchToWindowActionListenerList ), false, new Runnable() { @Override public void run() { for ( final ActionListener actionListener : stretchToWindowActionListenerList ) stretchToWindowCheckBox.removeActionListener( actionListener ); tabbedPane.setSelectedIndex( 0 ); // Select the Players tab when this player is closed (that's where we go to this tab from) } } ); tabbedPane.setSelectedIndex( tabbedPane.getTabCount() - 1 ); SwingUtilities.invokeLater( new Runnable() { @Override public void run() { chartsPanelHolder.value.requestFocusInWindow(); } } ); } /** * Creates and returns the maps tab. * @return the maps tab */ private JComponent createMapsTab() { final JPanel panel = new JPanel( new BorderLayout() ); panel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( mapStatisticsMap.size() ); final NullAwareComparable< Integer > nullWinRatio = NullAwareComparable.getPercent( null ); for ( final MapStatistics ms : mapStatisticsMap.values() ) { final Record pRecord = ms.raceRecordMap.get( Race.PROTOSS ); final Record tRecord = ms.raceRecordMap.get( Race.TERRAN ); final Record zRecord = ms.raceRecordMap.get( Race.ZERG ); final Vector< Object > row = new Vector< Object >( 9 ); row.add( ms.name ); row.add( ms.record.totalGames ); row.add( NullAwareComparable.getPercent( ms.record.totalGames * 100 / replaysIncludedInAnalysis ) ); row.add( pRecord == null ? nullWinRatio : pRecord.getWinRatio() ); row.add( tRecord == null ? nullWinRatio : tRecord.getWinRatio() ); row.add( zRecord == null ? nullWinRatio : zRecord.getWinRatio() ); row.add( ReplayUtils.formatMs( ms.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( Language.formatDate( ms.firstDate ) ); row.add( Language.formatDate( ms.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( panel, "module.multiRepAnal.tab.maps.info", new Object[] { mapStatisticsMap.size() }, 0, new int[] { 1, 0 }, dataVector, MAPS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.maps.title" ), 0, 1 ), null, null ); } } ); return panel; } /** * Creates and returns the Build orders tab. * @return the Build orders tab */ private JComponent createBuildOrdersTab() { final JPanel panel = new JPanel( new BorderLayout() ); panel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final JTabbedPane buildOrdersTabbedPane = new JTabbedPane(); for ( final Format format : EnumCache.FORMATS ) { final Map< String, BuildOrderStatistics > buildOrderStatisticsMap = formatBuildOrderStatisticsMap.get( format ); if ( buildOrderStatisticsMap == null ) continue; final JPanel buildOrdersPanel = new JPanel( new BorderLayout() ); buildOrdersPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( buildOrderStatisticsMap.size() ); for ( final BuildOrderStatistics bs : buildOrderStatisticsMap.values() ) { if ( format == Format.ONE_VS_ONE ) { final Record recordVsP = bs.recordVsRaceMap.get( Race.PROTOSS ); final Record recordVsT = bs.recordVsRaceMap.get( Race.TERRAN ); final Record recordVsZ = bs.recordVsRaceMap.get( Race.ZERG ); final Vector< Object > row = new Vector< Object >( 10 ); row.add( bs.race ); row.add( bs.buildOrder ); row.add( bs.record.totalGames ); row.add( bs.record ); row.add( bs.record.getWinRatio() ); row.add( recordVsP == null ? new Record() : recordVsP ); row.add( recordVsT == null ? new Record() : recordVsT ); row.add( recordVsZ == null ? new Record() : recordVsZ ); row.add( Language.formatDate( bs.firstDate ) ); row.add( Language.formatDate( bs.lastDate ) ); dataVector.add( row ); } else { final Vector< Object > row = new Vector< Object >( 7 ); row.add( bs.race ); row.add( bs.buildOrder ); row.add( bs.record.totalGames ); row.add( bs.record ); row.add( bs.record.getWinRatio() ); row.add( Language.formatDate( bs.firstDate ) ); row.add( Language.formatDate( bs.lastDate ) ); dataVector.add( row ); } } createStatisticsTableTab( buildOrdersPanel, "module.multiRepAnal.tab.buildOrders.info", new Object[] { buildOrderStatisticsMap.size() }, 1, new int[] { 2, 0, 4 }, dataVector, format == Format.ONE_VS_ONE ? BUILD_ORDERS_1V1_HEADER_NAME_VECTOR : BUILD_ORDERS_NON_1V1_HEADER_NAME_VECTOR, null, null, null ); } } ); GuiUtils.addNewTab( format.stringValue, null, false, buildOrdersTabbedPane, buildOrdersPanel, false, null ); } panel.add( buildOrdersTabbedPane ); } } ); return panel; } /** * Creates and returns the Chat words tab. * @return the Chat words tab */ private JComponent createChatWordsTab() { final JPanel panel = new JPanel( new BorderLayout() ); panel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( chatWordsStatisticsMap.size() ); for ( final WordStatistics ws : chatWordsStatisticsMap.values() ) { final Vector< Object > row = new Vector< Object >( 6 ); row.add( ws.word ); row.add( ws.count ); row.add( ws.replays ); row.add( NullAwareComparable.getPercent( ws.replays * 100 / replaysIncludedInAnalysis ) ); row.add( Language.formatDate( ws.firstDate ) ); row.add( Language.formatDate( ws.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( panel, "module.multiRepAnal.tab.chatWords.info", new Object[] { chatWordsStatisticsMap.size() }, 0, new int[] { 1, 2, 0 }, dataVector, CHAT_WORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.chatWords.title" ), 0, 1 ), null, null ); } } ); return panel; } /** * Creates and returns the player tab for the specified player. * @param ps player statistics of the player to create tab for * @param chartsPanelHolder outgoing reference holder to the charts panel, will be used to request focus on it * @param stretchToWindowActionListenerList the tables' action listeners to stretch to window will be added to this * @return the player tab for the specified player */ private JComponent createPlayerTab( final PlayerStatistics ps, final Holder< JComponent > chartsPanelHolder, final List< ActionListener > stretchToWindowActionListenerList ) { final JPanel panel = new JPanel( new BorderLayout() ); // Player game participation stat list must be in chronological order for the gaming sessions. // Development chart also builds on this list being sorted by date Collections.sort( ps.playerGameParticipationStatsList ); final int gameLengthRecordsGranularityMin = Settings.getInt( Settings.KEY_SETTINGS_MISC_GAME_LENGTH_RECORDS_GRANULARITY ); final int gameLengthRecordsGranularitySec = gameLengthRecordsGranularityMin * 60; final long maxGamingSessionBreakMs = Settings.getInt( Settings.KEY_SETTINGS_MISC_MAX_GAMING_SESSION_BREAK ) * 60L*1000; // Calculate advanced player statistics final Map< Format, PlayerStatistics > formatPlayerStatsMap = new EnumMap< Format , PlayerStatistics >( Format.class ); final Map< GameType, PlayerStatistics > typePlayerStatsMap = new EnumMap< GameType, PlayerStatistics >( GameType.class ); // League match-up stats for all formats final Map< Format, Map< String, PlayerStatistics > > formatLeagueMatchupPlayerStatsMapMap = new EnumMap< Format, Map< String, PlayerStatistics > >( Format.class ); // Match-up stats for all formats final Map< Format, Map< String, PlayerStatistics > > formatMatchupPlayerStatsMapMap = new EnumMap< Format, Map< String, PlayerStatistics > >( Format.class ); // Match-up stats by maps (key: pair of map and match-up) final Map< Pair< String, String >, PlayerStatistics > matchupByMapsPlayerStatsMap = new HashMap< Pair< String, String >, PlayerStatistics >(); // Key is the game length interval index: if granularity is 5 min: "0-5 min" => 0, "5-10 min" => 1 ... final Map< Integer, PlayerStatistics > gameLengthPlayerStatsMap = new HashMap< Integer, PlayerStatistics >(); final PlayerStatistics[] dayPlayerStats = new PlayerStatistics[ DAY_TEXT_KEYS.length ]; final PlayerStatistics[] hourPlayerStats = new PlayerStatistics[ 24 ]; final Map< String, MapStatistics > mapStatisticsMap = new HashMap< String , MapStatistics >(); // Build order stats for all formats final Map< Format, Map< String, BuildOrderStatistics > > formatBuildOrderStatisticsMap = new EnumMap< Format, Map< String, BuildOrderStatistics > >( Format.class ); final Map< String, PlaymateStatistics > playmateStatisticsMap = new HashMap< String , PlaymateStatistics >(); // Game in session stats; it's a list, the index is the game number in the gaming session (no need for map where the index would be the key...) final List< PlayerStatistics > gameInSessionStatsList = new ArrayList< PlayerStatistics >(); // Chat word stats final Map< String, WordStatistics > chatWordsStatisticsMap = new HashMap< String , WordStatistics >(); final GregorianCalendar gc = new GregorianCalendar(); final int[] hourlyActivities = new int[ 24 ]; final int[] dailyActivities = new int[ 7 ]; final int[] monthlyActivities = new int[ 12 ]; int currentGamingSessionLength = 0; PlayerGameParticipationStats lastPgps = null; for ( final PlayerGameParticipationStats pgps : ps.playerGameParticipationStatsList ) { // Activity trend data gc.setTime( pgps.date ); hourlyActivities [ gc.get( Calendar.HOUR_OF_DAY ) ]++; dailyActivities [ gc.get( Calendar.DAY_OF_WEEK )-1 ]++; monthlyActivities[ gc.get( Calendar.MONTH ) ]++; PlayerStatistics vps; // A virtual Player Statistics // Game type statistics (Game type is never null) vps = typePlayerStatsMap.get( pgps.gameType ); if ( vps == null ) typePlayerStatsMap.put( pgps.gameType, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); // Format statistics (Format is never null) vps = formatPlayerStatsMap.get( pgps.format ); if ( vps == null ) formatPlayerStatsMap.put( pgps.format, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); // League match-up statistics if ( pgps.teamLeagueCompositions.length > 0 ) { Map< String, PlayerStatistics > leagueMatchupPlayerStatsMap = formatLeagueMatchupPlayerStatsMapMap.get( pgps.format ); if ( leagueMatchupPlayerStatsMap == null ) formatLeagueMatchupPlayerStatsMapMap.put( pgps.format, leagueMatchupPlayerStatsMap = new HashMap< String, PlayerStatistics >() ); if ( pgps.format == Format.FREE_FOR_ALL || pgps.format == Format.UNKNOWN ) { final StringBuilder leagueMatchupBuilder = new StringBuilder(); for ( final String teamComposition : pgps.teamLeagueCompositions ) { if ( leagueMatchupBuilder.length() > 0 ) leagueMatchupBuilder.append( 'v' ); leagueMatchupBuilder.append( teamComposition ); } String leagueMatchup = leagueMatchupBuilder.toString(); vps = leagueMatchupPlayerStatsMap.get( leagueMatchup ); if ( vps == null ) leagueMatchupPlayerStatsMap.put( leagueMatchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); if ( pgps.format == Format.FREE_FOR_ALL ) { leagueMatchup = pgps.teamLeagueCompositions[ 0 ] + "v" + League.ANY.letter; vps = leagueMatchupPlayerStatsMap.get( leagueMatchup ); if ( vps == null ) leagueMatchupPlayerStatsMap.put( leagueMatchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); } } else { final String teamSeparator = pgps.format == Format.ONE_VS_ONE ? "v" : " vs "; final int cyclesCount = pgps.format == Format.ONE_VS_ONE ? 3 : 4; for ( int i = 0; i < cyclesCount; i++ ) { // 3 cycles: one for normal match-up, one for summarized opponent ANY (*) and one for summarized own ANY (*) // 4th : league + summarized teammates ANY (*) (which is for example: D+* vs *, P+* vs *, G+* vs *) final String leagueMatchup; if ( pgps.teamLeagueCompositions.length > 1 ) switch ( i ) { case 0 : leagueMatchup = pgps.teamLeagueCompositions[ 0 ] + teamSeparator + pgps.teamLeagueCompositions[ 1 ]; break; case 1 : leagueMatchup = pgps.teamLeagueCompositions[ 0 ] + teamSeparator + League.ANY.letter; break; case 2 : leagueMatchup = League.ANY.letter + teamSeparator + pgps.teamLeagueCompositions[ 1 ]; break; case 3 : { if ( pgps.teamLeagueCompositions[ 0 ].length() < 3 || pgps.teamLeagueCompositions[ 0 ].charAt( 1 ) != '+' ) continue; leagueMatchup = pgps.teamLeagueCompositions[ 0 ].charAt( 0 ) + "+" + League.ANY.letter + teamSeparator + League.ANY.letter; break; } default : throw new RuntimeException( "Fix the cycle!" ); } else { if ( i > 0 ) // League match-ups where there is no opponent should only be counted once break; leagueMatchup = pgps.teamLeagueCompositions.length == 1 ? pgps.teamLeagueCompositions[ 0 ] : Language.getText( "general.unknown" ); } vps = leagueMatchupPlayerStatsMap.get( leagueMatchup ); if ( vps == null ) leagueMatchupPlayerStatsMap.put( leagueMatchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); } } } // Match-up statistics; and Match-up by maps statistics Map< String, PlayerStatistics > matchupPlayerStatsMap = formatMatchupPlayerStatsMapMap.get( pgps.format ); if ( matchupPlayerStatsMap == null ) formatMatchupPlayerStatsMapMap.put( pgps.format, matchupPlayerStatsMap = new HashMap< String, PlayerStatistics >() ); if ( pgps.format == Format.FREE_FOR_ALL || pgps.format == Format.UNKNOWN ) { final StringBuilder matchupBuilder = new StringBuilder(); for ( final String teamComposition : pgps.teamRaceCompositions ) { if ( matchupBuilder.length() > 0 ) matchupBuilder.append( 'v' ); matchupBuilder.append( teamComposition ); } String matchup = matchupBuilder.toString(); vps = matchupPlayerStatsMap.get( matchup ); if ( vps == null ) matchupPlayerStatsMap.put( matchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); if ( pgps.format == Format.FREE_FOR_ALL ) { matchup = pgps.teamRaceCompositions[ 0 ] + "v" + Race.ANY.letter; vps = matchupPlayerStatsMap.get( matchup ); if ( vps == null ) matchupPlayerStatsMap.put( matchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); } } else { final String teamSeparator = pgps.format == Format.ONE_VS_ONE ? "v" : " vs "; final int cyclesCount = pgps.format == Format.ONE_VS_ONE ? 3 : 4; for ( int i = 0; i < cyclesCount; i++ ) { // 3 cycles: one for normal match-up, one for summarized opponent ANY (*) and one for summarized own ANY (*) // 4th : race + summarized teammates ANY (*) (which is for example: P+* vs *, T+* vs *, Z+* vs *) final String matchup; if ( pgps.teamRaceCompositions.length > 1 ) switch ( i ) { case 0 : matchup = pgps.teamRaceCompositions[ 0 ] + teamSeparator + pgps.teamRaceCompositions[ 1 ]; break; case 1 : matchup = pgps.teamRaceCompositions[ 0 ] + teamSeparator + Race.ANY.letter; break; case 2 : matchup = Race.ANY.letter + teamSeparator + pgps.teamRaceCompositions[ 1 ]; break; case 3 : { if ( pgps.teamRaceCompositions[ 0 ].length() < 3 || pgps.teamRaceCompositions[ 0 ].charAt( 1 ) != '+' ) continue; matchup = pgps.teamRaceCompositions[ 0 ].charAt( 0 ) + "+" + Race.ANY.letter + teamSeparator + Race.ANY.letter; break; } default : throw new RuntimeException( "Fix the cycle!" ); } else { if ( i > 0 ) // Match-ups where there is no opponent should only be counted once break; matchup = pgps.teamRaceCompositions.length == 1 ? pgps.teamRaceCompositions[ 0 ] : Language.getText( "general.unknown" ); } vps = matchupPlayerStatsMap.get( matchup ); if ( vps == null ) matchupPlayerStatsMap.put( matchup, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); // Match-ups by maps: if ( pgps.format == Format.ONE_VS_ONE ) { final Pair< String, String > key = new Pair< String, String >( pgps.mapName, matchup ); vps = matchupByMapsPlayerStatsMap.get( key ); if ( vps == null ) matchupByMapsPlayerStatsMap.put( key, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); } } } // Game length statistics final Integer gameLengthIntervalIndex = pgps.timeSecInGame / gameLengthRecordsGranularitySec; vps = gameLengthPlayerStatsMap.get( gameLengthIntervalIndex ); if ( vps == null ) gameLengthPlayerStatsMap.put( gameLengthIntervalIndex, vps = new PlayerStatistics( null, null ) ); vps.buildInPlayerGameParticipation( pgps ); // Day statistics vps = dayPlayerStats[ gc.get( Calendar.DAY_OF_WEEK )-1 ]; if ( vps == null ) vps = dayPlayerStats[ gc.get( Calendar.DAY_OF_WEEK )-1 ] = new PlayerStatistics( null, null ); vps.buildInPlayerGameParticipation( pgps ); // Hour statistics vps = hourPlayerStats[ gc.get( Calendar.HOUR_OF_DAY ) ]; if ( vps == null ) vps = hourPlayerStats[ gc.get( Calendar.HOUR_OF_DAY ) ] = new PlayerStatistics( null, null ); vps.buildInPlayerGameParticipation( pgps ); // Map statistics MapStatistics ms = mapStatisticsMap.get( pgps.mapName ); if ( ms == null ) mapStatisticsMap.put( pgps.mapName, ms = new MapStatistics( pgps.mapName ) ); ms.record.totalGames++; ms.totalTimeSecInGames += pgps.timeSecInGame; // No conversion (with game speed) because this time value has already been converted if ( pgps.isWinner != null ) if ( pgps.isWinner ) ms.record.wins++; else ms.record.losses++; ms.registerDate( pgps.date ); if ( pgps.format == Format.ONE_VS_ONE && pgps.isWinner != null ) { final Record record = ms.getRaceRecord( pgps.race ); record.totalGames++; if ( pgps.isWinner ) record.wins++; else record.losses++; } // Build orders Map< String, BuildOrderStatistics > buildOrderStatisticsMap = formatBuildOrderStatisticsMap.get( pgps.format ); if ( buildOrderStatisticsMap == null ) formatBuildOrderStatisticsMap.put( pgps.format, buildOrderStatisticsMap = new HashMap< String, BuildOrderStatistics >() ); BuildOrderStatistics bs = buildOrderStatisticsMap.get( pgps.buildOrder ); if ( bs == null ) buildOrderStatisticsMap.put( pgps.buildOrder, bs = new BuildOrderStatistics( pgps.buildOrder, pgps.race, pgps.format ) ); bs.buildInPlayerGameParticipation( pgps ); // Playmate statistics for ( final String playmate : pgps.allyList ) { PlaymateStatistics playmateStatistics = playmateStatisticsMap.get( playmate ); if ( playmateStatistics == null ) playmateStatisticsMap.put( playmate, playmateStatistics = new PlaymateStatistics( playmate ) ); playmateStatistics.buildInPlayerGameParticipation( pgps, true ); } for ( final String playmate : pgps.opponentList ) { PlaymateStatistics playmateStatistics = playmateStatisticsMap.get( playmate ); if ( playmateStatistics == null ) playmateStatisticsMap.put( playmate, playmateStatistics = new PlaymateStatistics( playmate ) ); playmateStatistics.buildInPlayerGameParticipation( pgps, false ); } // Gaming Sessions statistics if ( lastPgps == null || pgps.startDate.getTime() - maxGamingSessionBreakMs > lastPgps.startDate.getTime() + ( useRealTime ? lastPgps.timeSecInGame : lastPgps.gameSpeed.convertToRealTime( lastPgps.timeSecInGame ) )*1000L ) // Real time in last game currentGamingSessionLength = 1; // Too big time break, start a new session else currentGamingSessionLength++; if ( gameInSessionStatsList.size() < currentGamingSessionLength ) gameInSessionStatsList.add( vps = new PlayerStatistics( null, null ) ); else vps = gameInSessionStatsList.get( currentGamingSessionLength - 1 ); vps.buildInPlayerGameParticipation( pgps ); // Chat words statistics if ( pgps.wordCountMap != null ) for ( final Entry< String, IntHolder > entry : pgps.wordCountMap.entrySet() ) { WordStatistics wordStatistics = chatWordsStatisticsMap.get( entry.getKey() ); if ( wordStatistics == null ) chatWordsStatisticsMap.put( entry.getKey(), wordStatistics = new WordStatistics( entry.getKey() ) ); wordStatistics.buildInCount( entry.getValue().value, pgps.date ); } lastPgps = pgps; } // Advanced statistics ready. Now build the GUI. final JLabel infoLabel = new JLabel( Language.getText( "module.multiRepAnal.tab.player.info", ps.playerDisplayName, ps.record.totalGames ) ); GuiUtils.changeFontToBold( infoLabel ); panel.add( infoLabel, BorderLayout.NORTH ); final JTabbedPane tabbedPane = new JTabbedPane(); // Development charts tab { final JPanel chartsPanel = new JPanel( new BorderLayout() ); chartsPanelHolder.value = chartsPanel; // Pgps list is sorted... final Holder< JComponent > chartCanvasHolder = new Holder< JComponent >(); final ActionListener chartCanvasRepaintListener = new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { chartCanvasHolder.value.repaint(); } }; final Box chartOptionsBox = Box.createHorizontalBox(); chartOptionsBox.add( new JPanel( new BorderLayout() ) ); // Need to fill up space due to JComboBoxes chartOptionsBox.add( new JLabel( Language.getText( "charts.chartType" ) ) ); final JComboBox< ChartType > chartTypeComboBox = GuiUtils.createComboBox( ChartType.values(), Settings.KEY_MULTI_REP_ANAL_CHARTS_CHART_TYPE ); chartTypeComboBox.setToolTipText( Language.getText( "charts.chartTypeToolTip" ) ); chartTypeComboBox.addActionListener( chartCanvasRepaintListener ); chartOptionsBox.add( chartTypeComboBox ); chartOptionsBox.add( Box.createHorizontalStrut( 5 ) ); chartOptionsBox.add( new JLabel( Language.getText( "module.multiRepAnal.tab.player.tab.charts.granularity" ) ) ); final JComboBox< ChartGranularity > chartGranularityComboBox = GuiUtils.createComboBox( ChartGranularity.values(), Settings.KEY_MULTI_REP_ANAL_CHARTS_CHART_GRANULARITY ); final Holder< ChartData > chartDataHolder = new Holder< ChartData >(); final ActionListener chartDataCreatorListener = new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { final ChartGranularity chartGranularity = (ChartGranularity) chartGranularityComboBox.getSelectedItem(); if ( chartDataHolder.value == null || chartDataHolder.value.chartGranularity != chartGranularity ) { chartDataHolder.value = new ChartData( ps, chartGranularity ); } } }; chartGranularityComboBox.addActionListener( chartDataCreatorListener ); chartGranularityComboBox.addActionListener( chartCanvasRepaintListener ); chartOptionsBox.add( chartGranularityComboBox ); chartOptionsBox.add( Box.createHorizontalStrut( 5 ) ); chartOptionsBox.add( new JLabel( Language.getText( "charts.graphApproximation" ) ) ); final JComboBox< GraphApproximation > graphApproximationComboBox = GuiUtils.createComboBox( GraphApproximation.values(), Settings.KEY_MULTI_REP_ANAL_CHARTS_GRAPH_APPROXIMATION ); graphApproximationComboBox.addActionListener( chartCanvasRepaintListener ); chartOptionsBox.add( graphApproximationComboBox ); chartDataCreatorListener.actionPerformed( null ); chartOptionsBox.add( new JPanel( new BorderLayout() ) ); // Need to fill up space due to JComboBoxes chartsPanel.add( chartOptionsBox, BorderLayout.NORTH ); final JComponent chartCanvas = chartCanvasHolder.value = new JComponent() { protected void paintComponent( final Graphics graphics ) { // Create the chart params final ChartParams params = new ChartParams(); params.chartCanvas = chartCanvasHolder.value; params.g2 = (Graphics2D) graphics; params.playerStatistics = ps; params.segmentStats = chartDataHolder.value.segmentStats; params.chartType = (ChartType) chartTypeComboBox.getSelectedItem(); params.chartGranularity = (ChartGranularity ) chartGranularityComboBox .getSelectedItem(); params.graphApproximation = (GraphApproximation) graphApproximationComboBox.getSelectedItem(); params.width = getWidth(); params.height = getHeight(); new ChartPainter( params ).paintChart(); } }; chartCanvas.setFocusable( true ); chartsPanel.add( chartCanvas, BorderLayout.CENTER ); // Register hotkeys for chart types Object actionKey = new Object(); for ( final ChartType chartType : ChartType.values() ) { chartsPanel.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( chartType.keyStroke, actionKey = new Object() ); chartsPanel.getActionMap().put( actionKey, new AbstractAction() { @Override public void actionPerformed( final ActionEvent event ) { chartTypeComboBox.setSelectedItem( chartType ); } } ); } GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.charts.title" ), Icons.CHART_UP_COLOR, false, tabbedPane, chartsPanel, null ); } // Activity trend charts final JPanel trendsPanel = new JPanel( new BorderLayout() ); trendsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Holder< JComponent > chartCanvasHolder = new Holder< JComponent >(); final ActionListener chartCanvasRepaintListener = new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { chartCanvasHolder.value.repaint(); } }; final Box chartOptionsBox = Box.createHorizontalBox(); chartOptionsBox.add( new JPanel( new BorderLayout() ) ); // Need to fill up space due to JComboBox chartOptionsBox.add( new JLabel( Language.getText( "module.multiRepAnal.tab.player.tab.trends.type" ) ) ); final JComboBox< TrendType > trendTypeComboBox = GuiUtils.createComboBox( TrendType.values(), Settings.KEY_MULTI_REP_ANAL_TRENDS_TREND_TYPE ); trendTypeComboBox.setToolTipText( Language.getText( "charts.chartTypeToolTip" ) ); trendTypeComboBox.addActionListener( chartCanvasRepaintListener ); chartOptionsBox.add( trendTypeComboBox ); chartOptionsBox.add( new JPanel( new BorderLayout() ) ); // Need to fill up space due to JComboBox trendsPanel.add( chartOptionsBox, BorderLayout.NORTH ); final JComponent chartCanvas = chartCanvasHolder.value = new JComponent() { protected void paintComponent( final Graphics graphics ) { // Create the trend params final TrendParams params = new TrendParams(); params.chartCanvas = chartCanvasHolder.value; params.g2 = (Graphics2D) graphics; params.trendType = (TrendType) trendTypeComboBox.getSelectedItem(); switch ( params.trendType ) { case HOURLY : params.activityData = hourlyActivities ; break; case DAILY : params.activityData = dailyActivities ; break; case MONTHLY : params.activityData = monthlyActivities; break; default : throw new RuntimeException( "Insert proper activity data here!" ); } params.width = getWidth(); params.height = getHeight(); new TrendPainter( params ).paintChart(); } }; trendsPanel.add( chartCanvas, BorderLayout.CENTER ); // Register hotkeys for chart types Object actionKey = new Object(); for ( final TrendType trendType : TrendType.values() ) { trendsPanel.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( trendType.keyStroke, actionKey = new Object() ); trendsPanel.getActionMap().put( actionKey, new AbstractAction() { @Override public void actionPerformed( final ActionEvent event ) { trendTypeComboBox.setSelectedItem( trendType ); } } ); } } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.trends.title" ), Icons.CHART, false, tabbedPane, trendsPanel, null ); // Playmates tab final JPanel playmatesPanel = new JPanel( new BorderLayout() ); playmatesPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( playmateStatisticsMap.size() ); for ( final Entry< String, PlaymateStatistics > entry : playmateStatisticsMap.entrySet() ) { final PlaymateStatistics vps = entry.getValue(); final Vector< Object > row = new Vector< Object >( 11 ); row.add( entry.getKey() ); row.add( vps.recordAsAllies.totalGames + vps.recordAsOpponents.totalGames ); row.add( vps.recordAsAllies.totalGames ); row.add( vps.recordAsOpponents.totalGames ); row.add( vps.recordAsAllies ); row.add( vps.recordAsAllies.getWinRatio() ); row.add( vps.recordAsOpponents ); row.add( vps.recordAsOpponents.getWinRatio() ); row.add( vps.getFormattedTotalTimeInGames() ); row.add( Language.formatDate( vps.firstDate ) ); row.add( Language.formatDate( vps.lastDate ) ); dataVector.add( row ); } final Holder< JTable > tableHolder = new Holder< JTable >(); createStatisticsTableTab( playmatesPanel, "module.multiRepAnal.tab.players.info", new Object[] { playmateStatisticsMap.size() }, 0, new int[] { 1, 0 }, dataVector, PLAYMATES_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.playmates.title" ) + " - " + ps.playerDisplayName, 0, 1 ), tableHolder, stretchToWindowActionListenerList ); tableHolder.value.addMouseListener( playersTableMouseListener ); registerEnterToOpenPlayer( tableHolder.value ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.playmates.title" ), Icons.GROUP_LINK, false, tabbedPane, playmatesPanel, null ); // Game type records tab final JPanel gameTypeRecordsPanel = new JPanel( new BorderLayout() ); gameTypeRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( typePlayerStatsMap.size() ); for ( final Entry< GameType, PlayerStatistics > entry : typePlayerStatsMap.entrySet() ) dataVector.add( createGeneralPlayerStatTableRow( entry.getValue(), entry.getKey() ) ); createStatisticsTableTab( gameTypeRecordsPanel, null, null, 0, new int[] { 1, 0 }, dataVector, TYPE_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.typeRecords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.typeRecords.title" ), null, false, tabbedPane, gameTypeRecordsPanel, null ); // Format records tab final JPanel formatRecordsPanel = new JPanel( new BorderLayout() ); formatRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( formatPlayerStatsMap.size() ); for ( final Entry< Format, PlayerStatistics > entry : formatPlayerStatsMap.entrySet() ) dataVector.add( createGeneralPlayerStatTableRow( entry.getValue(), entry.getKey() ) ); createStatisticsTableTab( formatRecordsPanel, null, null, 0, new int[] { 1, 0 }, dataVector, FORMAT_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.formatRecords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.formatRecords.title" ), Icons.FORMAT, false, tabbedPane, formatRecordsPanel, null ); // League match-ups records tab final JTabbedPane leagueMatchupsTabbedPane = new JTabbedPane(); leagueMatchupsTabbedPane.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { for ( final Format format : EnumCache.FORMATS ) { final Map< String, PlayerStatistics > leagueMatchupPlayerStatsMap = formatLeagueMatchupPlayerStatsMapMap.get( format ); if ( leagueMatchupPlayerStatsMap == null ) continue; final JPanel leagueMatchupRecordsPanel = new JPanel( new BorderLayout() ); leagueMatchupRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( leagueMatchupPlayerStatsMap.size() ); // League match-up records for ( final Entry< String, PlayerStatistics > entry : leagueMatchupPlayerStatsMap.entrySet() ) { final PlayerStatistics vps = entry.getValue(); final Vector< Object > row = new Vector< Object >( 13 ); row.add( entry.getKey() ); row.add( vps.record.totalGames ); row.add( vps.getAvgApm() ); row.add( vps.getAvgEapm() ); row.add( vps.getAvgApmRedundancy() ); row.add( vps.record ); row.add( vps.record.getWinRatio() ); row.add( vps.getRaceDistributionString() ); row.add( vps.getFormattedTotalTimeInGames() ); row.add( ReplayUtils.formatMs( vps.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( vps.getPresence() ); row.add( vps.getAvgGamesPerDay() ); row.add( Language.formatDate( vps.firstDate ) ); row.add( Language.formatDate( vps.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( leagueMatchupRecordsPanel, null, null, 0, new int[] { 1, 0 }, dataVector, LEAGUE_MATCHUP_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.leagueMatchupRecords.title" ) + " - " + format.stringValue + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( format.stringValue, null, false, leagueMatchupsTabbedPane, leagueMatchupRecordsPanel, false, null ); } } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.leagueMatchupRecords.title" ), Icons.SC2LEAGUE, false, tabbedPane, leagueMatchupsTabbedPane, null ); // Match-up records tab final JTabbedPane matchupsTabbedPane = new JTabbedPane(); matchupsTabbedPane.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { for ( final Format format : EnumCache.FORMATS ) { final Map< String, PlayerStatistics > matchupPlayerStatsMap = formatMatchupPlayerStatsMapMap.get( format ); if ( matchupPlayerStatsMap == null ) continue; final JPanel matchupRecordsPanel = new JPanel( new BorderLayout() ); matchupRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( matchupPlayerStatsMap.size() ); // Match-up records for ( final Entry< String, PlayerStatistics > entry : matchupPlayerStatsMap.entrySet() ) { final PlayerStatistics vps = entry.getValue(); final Vector< Object > row = new Vector< Object >( 13 ); row.add( entry.getKey() ); row.add( vps.record.totalGames ); row.add( vps.getAvgApm() ); row.add( vps.getAvgEapm() ); row.add( vps.getAvgApmRedundancy() ); row.add( vps.record ); row.add( vps.record.getWinRatio() ); row.add( vps.getFormattedTotalTimeInGames() ); row.add( ReplayUtils.formatMs( vps.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( vps.getPresence() ); row.add( vps.getAvgGamesPerDay() ); row.add( Language.formatDate( vps.firstDate ) ); row.add( Language.formatDate( vps.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( matchupRecordsPanel, null, null, 0, new int[] { 1, 0 }, dataVector, MATCHUP_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.matchupRecords.title" ) + " - " + format.stringValue + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( format.stringValue, null, false, matchupsTabbedPane, matchupRecordsPanel, false, null ); } } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.matchupRecords.title" ), Icons.RACE_ANY, false, tabbedPane, matchupsTabbedPane, null ); // Match-up records by maps tab final JPanel matchupByMapsRecordsPanel = new JPanel( new BorderLayout() ); matchupByMapsRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( matchupByMapsPlayerStatsMap.size() ); for ( final Entry< Pair< String, String >, PlayerStatistics > entry : matchupByMapsPlayerStatsMap.entrySet() ) { final PlayerStatistics vps = entry.getValue(); final Vector< Object > row = new Vector< Object >( 14 ); row.add( entry.getKey().value1 ); row.add( entry.getKey().value2 ); row.add( vps.record.totalGames ); row.add( vps.getAvgApm() ); row.add( vps.getAvgEapm() ); row.add( vps.getAvgApmRedundancy() ); row.add( vps.record ); row.add( vps.record.getWinRatio() ); row.add( vps.getFormattedTotalTimeInGames() ); row.add( ReplayUtils.formatMs( vps.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( vps.getPresence() ); row.add( vps.getAvgGamesPerDay() ); row.add( Language.formatDate( vps.firstDate ) ); row.add( Language.formatDate( vps.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( matchupByMapsRecordsPanel, null, null, 0, new int[] { 2, 1, 0 }, dataVector, MATCHUP_BY_MAPS_RECORDS_HEADER_NAME_VECTOR, null, null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.matchupByMapsRecords.title" ), Icons.RACE_ANY, false, tabbedPane, matchupByMapsRecordsPanel, null ); // Game length records tab final JPanel gameLengthRecordsPanel = new JPanel( new BorderLayout() ); gameLengthRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( gameLengthPlayerStatsMap.size() ); for ( final Entry< Integer, PlayerStatistics > entry : gameLengthPlayerStatsMap.entrySet() ) { final int gameLengthIntervalIndex = entry.getKey(); final IntHolder interval = new IntHolder( gameLengthIntervalIndex ) { final String intervalString = Language.getText( "module.multiRepAnal.tab.player.tab.gameLengthRecords.gameLengthIntervalMin", ( gameLengthIntervalIndex * gameLengthRecordsGranularityMin ) + "-" + ( (gameLengthIntervalIndex+1) * gameLengthRecordsGranularityMin ) ); @Override public String toString() { return intervalString; } }; dataVector.add( createGeneralPlayerStatTableRow( entry.getValue(), interval ) ); } createStatisticsTableTab( gameLengthRecordsPanel, null, null, 0, new int[] { 0 }, dataVector, GAME_LENGTH_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.gameLengthRecords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.gameLengthRecords.title" ), Icons.CLOCK, false, tabbedPane, gameLengthRecordsPanel, null ); // Day records tab final JPanel dayRecordsPanel = new JPanel( new BorderLayout() ); dayRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( dayPlayerStats.length ); for ( int i = 0; i < dayPlayerStats.length; i++ ) { if ( dayPlayerStats[ i ] == null ) continue; final int dayIndex = i; final IntHolder day = new IntHolder( dayIndex ) { @Override public String toString() { return TrendType.DAILY.labels[ dayIndex ]; } }; dataVector.add( createGeneralPlayerStatTableRow( dayPlayerStats[ dayIndex ], day ) ); } createStatisticsTableTab( dayRecordsPanel, null, null, 0, new int[] { 0 }, dataVector, DAY_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.dayRecords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.dayRecords.title" ), Icons.CALENDAR_SELECT_WEEK, false, tabbedPane, dayRecordsPanel, null ); // Hour records tab final JPanel hourRecordsPanel = new JPanel( new BorderLayout() ); hourRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( hourPlayerStats.length ); for ( int i = 0; i < hourPlayerStats.length; i++ ) if ( hourPlayerStats[ i ] != null ) dataVector.add( createGeneralPlayerStatTableRow( hourPlayerStats[ i ], i < 10 ? "0" + i : Integer.toString( i ) ) ); createStatisticsTableTab( hourRecordsPanel, null, null, 0, new int[] { 0 }, dataVector, HOUR_RECORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.player.tab.hourRecords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.hourRecords.title" ), Icons.CLOCK_MOON_PHASE, false, tabbedPane, hourRecordsPanel, null ); // Maps tab final JPanel mapsPanel = new JPanel( new BorderLayout() ); mapsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( mapStatisticsMap.size() ); final NullAwareComparable< Integer > nullWinRatio = NullAwareComparable.getPercent( null ); for ( final MapStatistics ms : mapStatisticsMap.values() ) { final Record pRecord = ms.raceRecordMap.get( Race.PROTOSS ); final Record tRecord = ms.raceRecordMap.get( Race.TERRAN ); final Record zRecord = ms.raceRecordMap.get( Race.ZERG ); final Vector< Object > row = new Vector< Object >( 11 ); row.add( ms.name ); row.add( ms.record.totalGames ); row.add( NullAwareComparable.getPercent( ms.record.totalGames * 100 / ps.playerGameParticipationStatsList.size() ) ); row.add( ms.record ); row.add( ms.record.getWinRatio() ); row.add( pRecord == null ? nullWinRatio : pRecord.getWinRatio() ); row.add( tRecord == null ? nullWinRatio : tRecord.getWinRatio() ); row.add( zRecord == null ? nullWinRatio : zRecord.getWinRatio() ); row.add( ReplayUtils.formatMs( ms.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( Language.formatDate( ms.firstDate ) ); row.add( Language.formatDate( ms.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( mapsPanel, "module.multiRepAnal.tab.maps.info", new Object[] { mapStatisticsMap.size() }, 0, new int[] { 1, 0 }, dataVector, PLAYER_MAPS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.maps.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.maps.title" ), Icons.MAPS_STACK, false, tabbedPane, mapsPanel, null ); // Build orders tab final JTabbedPane buildOrdersTabbedPane = new JTabbedPane(); buildOrdersTabbedPane.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { for ( final Format format : EnumCache.FORMATS ) { final Map< String, BuildOrderStatistics > buildOrderStatisticsMap = formatBuildOrderStatisticsMap.get( format ); if ( buildOrderStatisticsMap == null ) continue; final JPanel buildOrdersPanel = new JPanel( new BorderLayout() ); buildOrdersPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( buildOrderStatisticsMap.size() ); for ( final BuildOrderStatistics bs : buildOrderStatisticsMap.values() ) { if ( format == Format.ONE_VS_ONE ) { final Record recordVsP = bs.recordVsRaceMap.get( Race.PROTOSS ); final Record recordVsT = bs.recordVsRaceMap.get( Race.TERRAN ); final Record recordVsZ = bs.recordVsRaceMap.get( Race.ZERG ); final Vector< Object > row = new Vector< Object >( 10 ); row.add( bs.race ); row.add( bs.buildOrder ); row.add( bs.record.totalGames ); row.add( bs.record ); row.add( bs.record.getWinRatio() ); row.add( recordVsP == null ? new Record() : recordVsP ); row.add( recordVsT == null ? new Record() : recordVsT ); row.add( recordVsZ == null ? new Record() : recordVsZ ); row.add( Language.formatDate( bs.firstDate ) ); row.add( Language.formatDate( bs.lastDate ) ); dataVector.add( row ); } else { final Vector< Object > row = new Vector< Object >( 7 ); row.add( bs.race ); row.add( bs.buildOrder ); row.add( bs.record.totalGames ); row.add( bs.record ); row.add( bs.record.getWinRatio() ); row.add( Language.formatDate( bs.firstDate ) ); row.add( Language.formatDate( bs.lastDate ) ); dataVector.add( row ); } } createStatisticsTableTab( buildOrdersPanel, "module.multiRepAnal.tab.buildOrders.info", new Object[] { buildOrderStatisticsMap.size() }, 1, new int[] { 2, 0, 4 }, dataVector, format == Format.ONE_VS_ONE ? BUILD_ORDERS_1V1_HEADER_NAME_VECTOR : BUILD_ORDERS_NON_1V1_HEADER_NAME_VECTOR, null, null, null ); } } ); GuiUtils.addNewTab( format.stringValue, null, false, buildOrdersTabbedPane, buildOrdersPanel, false, null ); } } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.buildOrders.title" ), Icons.BLOCK, false, tabbedPane, buildOrdersTabbedPane, null ); // Gaming Sessions records tab final JPanel gamingSessionsRecordsPanel = new JPanel( new BorderLayout() ); gamingSessionsRecordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final int[] sessionsCountByLength = new int[ gameInSessionStatsList.size() ]; int sumSessions = 0; for ( int i = gameInSessionStatsList.size() - 1; i >= 0; i-- ) { sessionsCountByLength[ i ] = gameInSessionStatsList.get( i ).record.totalGames - sumSessions; sumSessions += sessionsCountByLength[ i ]; } final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( gameInSessionStatsList.size() ); int gameInSession = 1; for ( final PlayerStatistics ps : gameInSessionStatsList ) { final Vector< Object > generalPlayerStatTableRow = createGeneralPlayerStatTableRow( ps, gameInSession ); final Vector< Object > sessionTableRow = new Vector< Object >( generalPlayerStatTableRow.size() + 1 ); sessionTableRow.addAll( generalPlayerStatTableRow ); sessionTableRow.insertElementAt( sessionsCountByLength[ gameInSession - 1 ], 1 ); dataVector.add( sessionTableRow ); gameInSession++; } createStatisticsTableTab( gamingSessionsRecordsPanel, null, null, 0, new int[] { 0 }, dataVector, GAMING_SESSIONS_HEADER_NAME_VECTOR, null, null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.player.tab.gamingSessions.title" ), Icons.CHAIN, false, tabbedPane, gamingSessionsRecordsPanel, null ); // Chat words tab final JPanel chatWordsPanel = new JPanel( new BorderLayout() ); chatWordsPanel.addComponentListener( new FirstShownListener() { @Override public void firstShown( final ComponentEvent event ) { final Vector< Vector< Object > > dataVector = new Vector< Vector< Object > >( chatWordsStatisticsMap.size() ); for ( final WordStatistics ws : chatWordsStatisticsMap.values() ) { final Vector< Object > row = new Vector< Object >( 6 ); row.add( ws.word ); row.add( ws.count ); row.add( ws.replays ); row.add( NullAwareComparable.getPercent( ws.replays * 100 / ps.playerGameParticipationStatsList.size() ) ); row.add( Language.formatDate( ws.firstDate ) ); row.add( Language.formatDate( ws.lastDate ) ); dataVector.add( row ); } createStatisticsTableTab( chatWordsPanel, "module.multiRepAnal.tab.chatWords.info", new Object[] { chatWordsStatisticsMap.size() }, 0, new int[] { 1, 2, 0 }, dataVector, CHAT_WORDS_HEADER_NAME_VECTOR, new WordCloudTableInput( Language.getText( "module.multiRepAnal.tab.chatWords.title" ) + " - " + ps.playerDisplayName, 0, 1 ), null, stretchToWindowActionListenerList ); } } ); GuiUtils.addNewTab( Language.getText( "module.multiRepAnal.tab.chatWords.title" ), Icons.BALLOONS, false, tabbedPane, chatWordsPanel, null ); panel.add( tabbedPane, BorderLayout.CENTER ); return panel; } /** * Creates a general player stats table row. * @param ps player statistics to create a row from * @param mainValue main value to add as the first value * @return the created general player stats table row */ private static Vector< Object > createGeneralPlayerStatTableRow( final PlayerStatistics ps, final Object mainValue ) { final Vector< Object > row = new Vector< Object >( 14 ); row.add( mainValue ); row.add( ps.record.totalGames ); row.add( ps.getAvgApm() ); row.add( ps.getAvgEapm() ); row.add( ps.getAvgApmRedundancy() ); row.add( ps.record ); row.add( ps.record.getWinRatio() ); row.add( ps.getRaceDistributionString() ); row.add( ps.getFormattedTotalTimeInGames() ); row.add( ReplayUtils.formatMs( ps.getAvgGameLength() * 1000, GameSpeed.NORMAL ) ); // Passing GameSpeed.NORMAL because it has already been converted row.add( ps.getPresence() ); row.add( ps.getAvgGamesPerDay() ); row.add( Language.formatDate( ps.firstDate ) ); row.add( Language.formatDate( ps.lastDate ) ); return row; } /** * Creates a statistics table tab. * @param panel panel to build the tab on * @param infoTextKey optional key of the info text on the top of the tab * @param infoTextArguments arguments of the info text * @param nameColumn name column (this will be sorted ascending by default) * @param defaultSortColumns default sort columns * @param dataVector data vector of the table * @param headerNameVector header names of the table * @param wordCloudTableInput optional word cloud table input * @param tableHolder an optional holder, if provided, a reference to the table will be set to this * @param stretchToWindowActionListenerList optional list, if provided, the table's action listener to stretch to window will be added to this */ private void createStatisticsTableTab( final JPanel panel, final String infoTextKey, final Object[] infoTextArguments, final int nameColumn, final int[] defaultSortColumns, final Vector< Vector< Object > > dataVector, final Vector< String > headerNameVector, final WordCloudTableInput wordCloudTableInput, final Holder< JTable > tableHolder, final List< ActionListener > stretchToWindowActionListenerList ) { if ( infoTextKey != null ) panel.add( new JLabel( Language.getText( infoTextKey, infoTextArguments ) ), BorderLayout.NORTH ); final JTable table = GuiUtils.createNonEditableTable(); // To have a proper sorting, we need a table model which returns proper classes for columns final DefaultTableModel model = new DefaultTableModel() { @Override public Class<?> getColumnClass( final int columnIndex ) { int maxTries = getRowCount(); if ( maxTries == 0 ) return super.getColumnClass( columnIndex ); else { if ( maxTries > 10 ) maxTries = 10; for ( int i = 0; i < maxTries; i++ ) { final Object value = getValueAt( i, columnIndex ); if ( value != null ) return value.getClass(); } return Object.class; } } }; model.setDataVector( dataVector, headerNameVector ); table.setModel( model ); // I want default descending sorting in all columns but the name column table.setRowSorter( new TableRowSorter< TableModel >( model ) { { // By default sort by the 2nd column (replays) final List< SortKey > sortKeys = new ArrayList< SortKey >(); for ( final int column : defaultSortColumns ) sortKeys.add( new SortKey( column, column == nameColumn ? SortOrder.ASCENDING : SortOrder.DESCENDING ) ); setSortKeys( sortKeys ); setMaxSortKeys( 3 ); } @Override public void toggleSortOrder( int column ) { if ( column != nameColumn ) { final List< SortKey > sortKeys = new ArrayList< SortKey >( getSortKeys() ); if ( sortKeys.isEmpty() || sortKeys.get( 0 ).getColumn() != column ) { sortKeys.add( 0, new SortKey( column, SortOrder.DESCENDING ) ); if ( sortKeys.size() > getMaxSortKeys() ) sortKeys.remove( getMaxSortKeys() ); setSortKeys( sortKeys ); return; } } super.toggleSortOrder( column ); }; } ); final TableBox tableBox = new TableBox( table, panel, wordCloudTableInput ); panel.add( tableBox, BorderLayout.CENTER ); final ActionListener stretchToWindowActionListener = new ActionListener() { @Override public void actionPerformed( final ActionEvent event ) { table.setAutoResizeMode( stretchToWindowCheckBox.isSelected() ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF ); } }; stretchToWindowActionListener.actionPerformed( null ); stretchToWindowCheckBox.addActionListener( stretchToWindowActionListener ); if ( stretchToWindowActionListenerList != null ) stretchToWindowActionListenerList.add( stretchToWindowActionListener ); if ( tableHolder != null ) tableHolder.value = table; // We have to invoke this later, because table might be being updated right now... SwingUtilities.invokeLater( new Runnable() { @Override public void run() { GuiUtils.packTable( table ); } } ); } }