From 3d1e32fe8889219622a68fbe57dbaf7d7ace35b9 Mon Sep 17 00:00:00 2001 From: Rsl1122 Date: Thu, 15 Aug 2019 20:27:47 +0300 Subject: [PATCH] Players online & Day by Day graph to network page --- .../data/store/mutators/MutatorFunctions.java | 31 ++++++ .../queries/analysis/PlayerCountQueries.java | 72 +++++++++++++ .../db/access/queries/objects/TPSQueries.java | 61 +++++++++++ .../plan/system/json/GraphJSONParser.java | 45 +++++++- .../pages/json/GraphsJSONHandler.java | 4 + .../utilities/html/graphs/line/LineGraph.java | 8 ++ .../utilities/html/pages/NetworkPage.java | 10 +- .../resources/assets/plan/web/network.html | 101 ++++++++++++++++-- 8 files changed, 322 insertions(+), 10 deletions(-) diff --git a/Plan/common/src/main/java/com/djrapitops/plan/data/store/mutators/MutatorFunctions.java b/Plan/common/src/main/java/com/djrapitops/plan/data/store/mutators/MutatorFunctions.java index 88285e773..98879ce80 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/data/store/mutators/MutatorFunctions.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/data/store/mutators/MutatorFunctions.java @@ -51,6 +51,11 @@ public class MutatorFunctions { filled.put(point.getKey(), point.getValue()); } + long now = System.currentTimeMillis(); + if (lastX != null && now - lastX > accuracy) { + addMissing(lastX, now, filled, accuracy, replacement); + } + return filled; } @@ -62,6 +67,32 @@ public class MutatorFunctions { } } + public static List addMissing(List points, long accuracy, Integer replacement) { + if (Verify.isEmpty(points)) return points; + + List filled = new ArrayList<>(); + Long lastX = null; + for (Point point : points) { + long date = (long) point.getX(); + + if (lastX != null && date - lastX > accuracy) { + addMissing(lastX, date, filled, accuracy, replacement); + } + lastX = date; + filled.add(point); + } + + return filled; + } + + private static void addMissing(long from, long to, List points, long accuracy, Integer replacement) { + long iterate = from; + while (iterate < to) { + points.add(new Point(iterate, replacement)); + iterate += accuracy; + } + } + public static int average(Map map) { return (int) map.values().stream() .mapToInt(i -> i) diff --git a/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/analysis/PlayerCountQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/analysis/PlayerCountQueries.java index 86a02c402..9b122fa0d 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/analysis/PlayerCountQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/analysis/PlayerCountQueries.java @@ -143,6 +143,46 @@ public class PlayerCountQueries { }; } + /** + * Fetch a EpochMs - Count map of unique players on ALL servers. + * + * @param after After epoch ms + * @param before Before epoch ms + * @param timeZoneOffset Offset from {@link java.util.TimeZone#getOffset(long)}, applied to the dates before grouping. + * @return Map: Epoch ms (Accuracy of a day) - How many unique players played that day + */ + public static Query> uniquePlayerCounts(long after, long before, long timeZoneOffset) { + return database -> { + Sql sql = database.getSql(); + String selectUniquePlayersPerDay = SELECT + + sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) + + "*1000 as date," + + "COUNT(DISTINCT " + SessionsTable.USER_UUID + ") as player_count" + + FROM + SessionsTable.TABLE_NAME + + WHERE + SessionsTable.SESSION_END + "<=?" + + AND + SessionsTable.SESSION_START + ">=?" + + GROUP_BY + "date"; + + return database.query(new QueryStatement>(selectUniquePlayersPerDay, 100) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setLong(1, timeZoneOffset); + statement.setLong(2, before); + statement.setLong(3, after); + } + + @Override + public NavigableMap processResults(ResultSet set) throws SQLException { + NavigableMap uniquePerDay = new TreeMap<>(); + while (set.next()) { + uniquePerDay.put(set.getLong("date"), set.getInt("player_count")); + } + return uniquePerDay; + } + }); + }; + } + public static Query averageUniquePlayerCount(long after, long before, long timeZoneOffset, UUID serverUUID) { return database -> { Sql sql = database.getSql(); @@ -236,6 +276,38 @@ public class PlayerCountQueries { }; } + public static Query> newPlayerCounts(long after, long before, long timeZoneOffset) { + return database -> { + Sql sql = database.getSql(); + String selectNewPlayersQuery = SELECT + + sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + UserInfoTable.REGISTERED + "+?)/1000"))) + + "*1000 as date," + + "COUNT(1) as player_count" + + FROM + UsersTable.TABLE_NAME + + WHERE + UsersTable.REGISTERED + "<=?" + + AND + UsersTable.REGISTERED + ">=?" + + GROUP_BY + "date"; + + return database.query(new QueryStatement>(selectNewPlayersQuery, 100) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setLong(1, timeZoneOffset); + statement.setLong(2, before); + statement.setLong(3, after); + } + + @Override + public NavigableMap processResults(ResultSet set) throws SQLException { + NavigableMap newPerDay = new TreeMap<>(); + while (set.next()) { + newPerDay.put(set.getLong("date"), set.getInt("player_count")); + } + return newPerDay; + } + }); + }; + } + public static Query averageNewPlayerCount(long after, long before, long timeZoneOffset, UUID serverUUID) { return database -> { Sql sql = database.getSql(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/objects/TPSQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/objects/TPSQueries.java index abe479e45..8d9ac7bab 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/objects/TPSQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/db/access/queries/objects/TPSQueries.java @@ -118,6 +118,67 @@ public class TPSQueries { }; } + public static Query>> fetchPlayersOnlineOfServer(long after, long before, UUID serverUUID) { + String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE + + FROM + TABLE_NAME + + INNER_JOIN + ServerTable.TABLE_NAME + " on " + ServerTable.TABLE_NAME + '.' + ServerTable.SERVER_ID + '=' + SERVER_ID + + WHERE + ServerTable.SERVER_UUID + "=?" + + AND + DATE + "?"; + return new QueryStatement>>(sql, 1000) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, serverUUID.toString()); + statement.setLong(2, before); + statement.setLong(3, after); + } + + @Override + public List> processResults(ResultSet set) throws SQLException { + List> ofServer = new ArrayList<>(); + + while (set.next()) { + ofServer.add(new DateObj<>(set.getLong(DATE), set.getInt(PLAYERS_ONLINE))); + } + + return ofServer; + } + }; + } + + public static Query>>> fetchPlayersOnlineOfAllServersBut(long after, long before, UUID leaveOut) { + String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE + + FROM + TABLE_NAME + + INNER_JOIN + ServerTable.TABLE_NAME + " on " + ServerTable.TABLE_NAME + '.' + ServerTable.SERVER_ID + '=' + SERVER_ID + + WHERE + ServerTable.SERVER_UUID + "!=?" + + AND + ServerTable.INSTALLED + "=?" + + AND + DATE + "?"; + return new QueryStatement>>>(sql, 1000) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, leaveOut.toString()); + statement.setBoolean(2, true); + statement.setLong(3, before); + statement.setLong(4, after); + } + + @Override + public Map>> processResults(ResultSet set) throws SQLException { + Map>> byServer = new HashMap<>(); + + while (set.next()) { + UUID serverUUID = UUID.fromString(set.getString(ServerTable.SERVER_UUID)); + List> ofServer = byServer.getOrDefault(serverUUID, new ArrayList<>()); + ofServer.add(new DateObj<>(set.getLong(DATE), set.getInt(PLAYERS_ONLINE))); + byServer.put(serverUUID, ofServer); + } + + return byServer; + } + }; + } + public static Query>> fetchPlayerOnlineDataOfServers(Collection servers) { if (servers.isEmpty()) { return db -> new HashMap<>(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/system/json/GraphJSONParser.java b/Plan/common/src/main/java/com/djrapitops/plan/system/json/GraphJSONParser.java index e22e33479..36d83ae15 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/system/json/GraphJSONParser.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/system/json/GraphJSONParser.java @@ -30,11 +30,14 @@ import com.djrapitops.plan.db.access.queries.analysis.PlayerCountQueries; import com.djrapitops.plan.db.access.queries.objects.*; import com.djrapitops.plan.system.database.DBSystem; import com.djrapitops.plan.system.settings.config.PlanConfig; +import com.djrapitops.plan.system.settings.paths.DisplaySettings; import com.djrapitops.plan.system.settings.paths.TimeSettings; import com.djrapitops.plan.utilities.html.graphs.Graphs; import com.djrapitops.plan.utilities.html.graphs.bar.BarGraph; +import com.djrapitops.plan.utilities.html.graphs.line.LineGraph; import com.djrapitops.plan.utilities.html.graphs.line.LineGraphFactory; import com.djrapitops.plan.utilities.html.graphs.line.PingGraph; +import com.djrapitops.plan.utilities.html.graphs.line.Point; import com.djrapitops.plan.utilities.html.graphs.pie.Pie; import com.djrapitops.plan.utilities.html.graphs.pie.WorldPie; import com.djrapitops.plan.utilities.html.graphs.special.WorldMap; @@ -45,6 +48,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * Perses Graph related Data JSON. @@ -74,8 +78,10 @@ public class GraphJSONParser { public String performanceGraphJSON(UUID serverUUID) { Database db = dbSystem.getDatabase(); LineGraphFactory lineGraphs = graphs.line(); + long now = System.currentTimeMillis(); + long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L); TPSMutator tpsMutator = new TPSMutator(db.query(TPSQueries.fetchTPSDataOfServer(serverUUID))) - .filterDataBetween(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(180L), System.currentTimeMillis()); + .filterDataBetween(halfYearAgo, now); return '{' + "\"playersOnline\":" + lineGraphs.playersOnlineGraph(tpsMutator).toHighChartsSeries() + ",\"tps\":" + lineGraphs.tpsGraph(tpsMutator).toHighChartsSeries() + @@ -87,6 +93,18 @@ public class GraphJSONParser { '}'; } + public String playersOnlineGraph(UUID serverUUID) { + Database db = dbSystem.getDatabase(); + long now = System.currentTimeMillis(); + long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L); + Boolean displayGaps = config.get(DisplaySettings.GAPS_IN_GRAPH_DATA); + + List points = db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)).stream() + .map(point -> new Point(point.getDate(), point.getValue())) + .collect(Collectors.toList()); + return "{\"playersOnline\":" + new LineGraph(points, displayGaps).toHighChartsSeries() + '}'; + } + public String uniqueAndNewGraphJSON(UUID serverUUID) { Database db = dbSystem.getDatabase(); LineGraphFactory lineGraphs = graphs.line(); @@ -112,6 +130,31 @@ public class GraphJSONParser { '}'; } + public String uniqueAndNewGraphJSON() { + Database db = dbSystem.getDatabase(); + LineGraphFactory lineGraphs = graphs.line(); + long now = System.currentTimeMillis(); + long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L); + NavigableMap uniquePerDay = db.query( + PlayerCountQueries.uniquePlayerCounts(halfYearAgo, now, timeZone.getOffset(now)) + ); + NavigableMap newPerDay = db.query( + PlayerCountQueries.newPlayerCounts(halfYearAgo, now, timeZone.getOffset(now)) + ); + + return "{\"uniquePlayers\":" + + lineGraphs.lineGraph(MutatorFunctions.toPointsWithRemovedOffset( + MutatorFunctions.addMissing(uniquePerDay, TimeUnit.DAYS.toMillis(1L), 0), + timeZone + )).toHighChartsSeries() + + ",\"newPlayers\":" + + lineGraphs.lineGraph(MutatorFunctions.toPointsWithRemovedOffset( + MutatorFunctions.addMissing(newPerDay, TimeUnit.DAYS.toMillis(1L), 0), + timeZone + )).toHighChartsSeries() + + '}'; + } + public String serverCalendarJSON(UUID serverUUID) { Database db = dbSystem.getDatabase(); long now = System.currentTimeMillis(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/pages/json/GraphsJSONHandler.java b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/pages/json/GraphsJSONHandler.java index 1b356805e..eb33e7c28 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/pages/json/GraphsJSONHandler.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/system/webserver/pages/json/GraphsJSONHandler.java @@ -69,6 +69,8 @@ public class GraphsJSONHandler implements PageHandler { switch (type) { case "performance": return new JSONResponse(graphJSON.performanceGraphJSON(serverUUID)); + case "playersOnline": + return new JSONResponse(graphJSON.playersOnlineGraph(serverUUID)); case "uniqueAndNew": return new JSONResponse(graphJSON.uniqueAndNewGraphJSON(serverUUID)); case "serverCalendar": @@ -92,6 +94,8 @@ public class GraphsJSONHandler implements PageHandler { switch (type) { case "activity": return new JSONResponse(graphJSON.activityGraphsJSONAsMap()); + case "uniqueAndNew": + return new JSONResponse(graphJSON.uniqueAndNewGraphJSON()); default: throw new BadRequestException("unknown 'type' parameter: " + type); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/graphs/line/LineGraph.java b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/graphs/line/LineGraph.java index a8bd4b714..1236aad1d 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/graphs/line/LineGraph.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/graphs/line/LineGraph.java @@ -16,6 +16,7 @@ */ package com.djrapitops.plan.utilities.html.graphs.line; +import com.djrapitops.plan.data.store.mutators.MutatorFunctions; import com.djrapitops.plan.utilities.html.graphs.HighChart; import java.util.List; @@ -62,6 +63,13 @@ public class LineGraph implements HighChart { return arrayBuilder.toString(); } + public List getPoints() { + if (displayGaps) { + return MutatorFunctions.addMissing(points, TimeUnit.MINUTES.toMillis(1L), null); + } + return points; + } + private void addMissingPoints(StringBuilder arrayBuilder, Long lastX, long date) { long iterate = lastX + TimeUnit.MINUTES.toMillis(1L); while (iterate < date) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/pages/NetworkPage.java b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/pages/NetworkPage.java index f5e4e3077..667fffe7d 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/pages/NetworkPage.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/utilities/html/pages/NetworkPage.java @@ -31,6 +31,7 @@ import com.djrapitops.plan.utilities.formatting.Formatters; import com.djrapitops.plan.utilities.formatting.PlaceholderReplacer; import java.util.List; +import java.util.UUID; /** * Html String parser for /network page. @@ -71,16 +72,23 @@ public class NetworkPage implements Page { try { PlaceholderReplacer placeholders = new PlaceholderReplacer(); + UUID serverUUID = serverInfo.getServerUUID(); placeholders.put("networkDisplayName", config.get(ProxySettings.NETWORK_NAME)); + placeholders.put("serverUUID", serverUUID.toString()); placeholders.put("gmPieColors", theme.getValue(ThemeVal.GRAPH_GM_PIE)); placeholders.put("playersGraphColor", theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE)); + placeholders.put("worldMapColLow", theme.getValue(ThemeVal.WORLD_MAP_LOW)); + placeholders.put("worldMapColHigh", theme.getValue(ThemeVal.WORLD_MAP_HIGH)); + placeholders.put("maxPingColor", theme.getValue(ThemeVal.GRAPH_MAX_PING)); + placeholders.put("minPingColor", theme.getValue(ThemeVal.GRAPH_MIN_PING)); + placeholders.put("avgPingColor", theme.getValue(ThemeVal.GRAPH_AVG_PING)); placeholders.put("timeZone", config.getTimeZoneOffsetHours()); placeholders.put("update", versionCheckSystem.getUpdateHtml().orElse("")); List extensionData = dbSystem.getDatabase() - .query(new ExtensionServerDataQuery(serverInfo.getServerUUID())); + .query(new ExtensionServerDataQuery(serverUUID)); ServerPluginTabs pluginTabs = new ServerPluginTabs(extensionData, formatters); String nav = pluginTabs.getNav(); diff --git a/Plan/common/src/main/resources/assets/plan/web/network.html b/Plan/common/src/main/resources/assets/plan/web/network.html index 78c7a14f4..09ae225d0 100644 --- a/Plan/common/src/main/resources/assets/plan/web/network.html +++ b/Plan/common/src/main/resources/assets/plan/web/network.html @@ -154,18 +154,37 @@
-
-
- Network Online Activity
-
-
- + +
+
+
+
+
+
+
+
+
@@ -1289,6 +1308,72 @@ jsonRequest("../v1/network/playerbaseOverview", loadPlayerbaseOverviewValues); setLoadingText('Rendering graphs..'); + var v = { + colors: { + playersOnline: '${playersGraphColor}', + newPlayers: '#8BC34A', + geolocationsLow: '${worldMapColLow}', + geolocationsHigh: '${worldMapColHigh}', + maxPing: '${maxPingColor}', + minPing: '${minPingColor}', + avgPing: '${avgPingColor}' + } + }; + + // HighCharts Series + var s = { + name: { + playersOnline: 'Players Online', + uniquePlayers: 'Unique Players', + newPlayers: 'New Players', + maxPing: 'Worst Ping', + minPing: 'Best Ping', + avgPing: 'Average Ping' + }, + tooltip: { + twoDecimals: { + valueDecimals: 2 + }, + zeroDecimals: { + valueDecimals: 0 + } + }, + type: { + areaSpline: 'areaspline', + spline: 'spline' + } + }; + + jsonRequest("../v1/graph?type=playersOnline&server=${serverUUID}", function (data, error) { + if (data) { + var series = { + playersOnline: { + name: s.name.playersOnline, type: s.type.areaSpline, tooltip: s.tooltip.zeroDecimals, + data: data.playersOnline, color: v.colors.playersOnline, yAxis: 0 + } + }; + playersChart('playersOnlineChart', series.playersOnline, 2); + } else if (error) { + $('#playersOnlineChart').text("Failed to load graph data: " + error); + } + }); + + jsonRequest("../v1/graph?type=uniqueAndNew", function (data, error) { + if (data) { + var uniquePlayers = { + name: s.name.uniquePlayers, type: s.type.spline, tooltip: s.tooltip.zeroDecimals, + data: data.uniquePlayers, color: v.colors.playersOnline + }; + var newPlayers = { + name: s.name.newPlayers, type: s.type.spline, tooltip: s.tooltip.zeroDecimals, + data: data.newPlayers, color: v.colors.newPlayers + }; + lineChart('uniqueChart', [uniquePlayers, newPlayers]); + } else if (error) { + $('#uniqueChart').text("Failed to load graph data: " + error) + } + }); + jsonRequest("../v1/graph?type=activity", function (json, error) { if (json) { activityPie('activityPie', {