From 4b4aa2d7d988cfd21a1dd39996016927e4de82c8 Mon Sep 17 00:00:00 2001 From: Risto Lahtela <24460436+Rsl1122@users.noreply.github.com> Date: Fri, 29 Jan 2021 11:58:06 +0200 Subject: [PATCH] Added a players online graph to Query page to help selecting view --- .../json/graphs/GraphJSONCreator.java | 5 +- .../json/graphs/line/LineGraphFactory.java | 6 ++- .../rendering/json/graphs/line/Point.java | 22 +++++++-- .../resolver/json/FiltersJSONResolver.java | 36 +++++++++++++- .../queries/objects/SessionQueries.java | 11 +++++ .../database/queries/objects/TPSQueries.java | 23 +++++++++ .../resources/assets/plan/web/js/query.js | 47 ++++++++++++++---- .../main/resources/assets/plan/web/query.html | 48 +++++++++++++++++++ .../resources/assets/plan/web/server.html | 2 +- 9 files changed, 179 insertions(+), 21 deletions(-) diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java index efd7a4e23..84e269b6b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java @@ -138,8 +138,9 @@ public class GraphJSONCreator { long now = System.currentTimeMillis(); long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L); - List points = Lists.map(db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)), - point -> new Point(point.getDate(), point.getValue()) + List points = Lists.map( + db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, serverUUID)), + Point::fromDateObj ); return "{\"playersOnline\":" + graphs.line().lineGraph(points).toHighChartsSeries() + ",\"color\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"}"; diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/LineGraphFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/LineGraphFactory.java index 63f406a96..d48f6df58 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/LineGraphFactory.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/LineGraphFactory.java @@ -44,8 +44,12 @@ public class LineGraphFactory { } public LineGraph lineGraph(List points) { + return lineGraph(points, shouldDisplayGapsInData()); + } + + public LineGraph lineGraph(List points, boolean displayGaps) { points.sort(new PointComparator()); - return new LineGraph(points, shouldDisplayGapsInData()); + return new LineGraph(points, displayGaps); } public LineGraph chunkGraph(TPSMutator mutator) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/Point.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/Point.java index f439b20cd..b1084a443 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/Point.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/line/Point.java @@ -16,6 +16,8 @@ */ package com.djrapitops.plan.delivery.rendering.json.graphs.line; +import com.djrapitops.plan.delivery.domain.DateObj; + import java.util.Objects; /** @@ -23,15 +25,21 @@ import java.util.Objects; */ public class Point { private final double x; - private final Double y; + private Double y; public Point(double x, Double y) { this.x = x; this.y = y; } - public Point(double x, double y) { - this(x, (Double) y); + public Point(double x, V y) { + this.x = x; + this.y = y == null ? null : y.doubleValue(); + } + + public static Point fromDateObj(DateObj dateObj) { + V value = dateObj.getValue(); + return new Point(dateObj.getDate(), value != null ? value.doubleValue() : null); } public double getX() { @@ -42,6 +50,10 @@ public class Point { return y; } + public void setY(Double y) { + this.y = y; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -63,7 +75,7 @@ public class Point { "y=" + y + '}'; } - public double[] toArray() { - return new double[]{x, y}; + public Double[] toArray() { + return new Double[]{x, y}; } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java index 0d6dc17df..5e0b82461 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java @@ -16,15 +16,23 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.domain.DateObj; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.formatting.Formatters; +import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs; +import com.djrapitops.plan.delivery.rendering.json.graphs.line.Point; import com.djrapitops.plan.delivery.web.resolver.MimeType; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; import com.djrapitops.plan.delivery.web.resolver.request.WebUser; +import com.djrapitops.plan.identification.ServerInfo; +import com.djrapitops.plan.storage.database.DBSystem; import com.djrapitops.plan.storage.database.queries.filter.Filter; import com.djrapitops.plan.storage.database.queries.filter.QueryFilters; +import com.djrapitops.plan.storage.database.queries.objects.SessionQueries; +import com.djrapitops.plan.storage.database.queries.objects.TPSQueries; +import com.djrapitops.plan.utilities.java.Lists; import org.apache.commons.lang3.StringUtils; import javax.inject.Inject; @@ -34,19 +42,29 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @Singleton public class FiltersJSONResolver implements Resolver { + private final ServerInfo serverInfo; + private final DBSystem dbSystem; private final QueryFilters filters; + private final Graphs graphs; private final Formatters formatters; @Inject public FiltersJSONResolver( + ServerInfo serverInfo, + DBSystem dbSystem, QueryFilters filters, + Graphs graphs, Formatters formatters ) { + this.serverInfo = serverInfo; + this.dbSystem = dbSystem; this.filters = filters; + this.graphs = graphs; this.formatters = formatters; } @@ -62,11 +80,23 @@ public class FiltersJSONResolver implements Resolver { } private Response getResponse() { + List> data = dbSystem.getDatabase().query(TPSQueries.fetchQueryPreviewPlayersOnline(serverInfo.getServerUUID())); + Long earliestStart = dbSystem.getDatabase().query(SessionQueries.earliestSessionStart()); + data.add(0, new DateObj<>(earliestStart, 1)); + + boolean displayGaps = true; + List viewPoints = graphs.line().lineGraph(Lists.map(data, Point::fromDateObj), displayGaps).getPoints() + .stream().map(point -> { + if (point.getY() == null) point.setY(0.0); + return point.toArray(); + }).collect(Collectors.toList()); + return Response.builder() .setMimeType(MimeType.JSON) .setJSONContent(new FilterResponseJSON( filters.getFilters(), - new ViewJSON(formatters) + new ViewJSON(formatters), + viewPoints )).build(); } @@ -76,8 +106,10 @@ public class FiltersJSONResolver implements Resolver { static class FilterResponseJSON { final List filters; final ViewJSON view; + final List viewPoints; - public FilterResponseJSON(Map filtersByKind, ViewJSON view) { + public FilterResponseJSON(Map filtersByKind, ViewJSON view, List viewPoints) { + this.viewPoints = viewPoints; this.filters = new ArrayList<>(); for (Map.Entry entry : filtersByKind.entrySet()) { filters.add(new FilterJSON(entry.getKey(), entry.getValue())); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/SessionQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/SessionQueries.java index ad00bb705..541ee895f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/SessionQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/SessionQueries.java @@ -892,4 +892,15 @@ public class SessionQueries { } }; } + + public static Query earliestSessionStart() { + String sql = SELECT + "MIN(" + SessionsTable.SESSION_START + ") as m" + + FROM + SessionsTable.TABLE_NAME; + return new QueryAllStatement(sql) { + @Override + public Long processResults(ResultSet set) throws SQLException { + return set.next() ? set.getLong("m") : -1L; + } + }; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/TPSQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/TPSQueries.java index 0d97099a3..db6baab7c 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/TPSQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/TPSQueries.java @@ -154,6 +154,29 @@ public class TPSQueries { }; } + public static Query>> fetchQueryPreviewPlayersOnline(UUID serverUUID) { + String sql = SELECT + "MIN(" + DATE + ") as " + DATE + ',' + + "MAX(" + PLAYERS_ONLINE + ") as " + PLAYERS_ONLINE + + FROM + TABLE_NAME + + WHERE + SERVER_ID + "=" + ServerTable.STATEMENT_SELECT_SERVER_ID + + GROUP_BY + "FLOOR(" + DATE + "/?)"; + + return new QueryStatement>>(sql) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, serverUUID.toString()); + statement.setLong(2, TimeUnit.MINUTES.toMillis(15)); + } + + @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>> fetchPlayersOnlineOfServer(long after, long before, UUID serverUUID) { String sql = SELECT + ServerTable.SERVER_UUID + ',' + DATE + ',' + PLAYERS_ONLINE + FROM + TABLE_NAME + diff --git a/Plan/common/src/main/resources/assets/plan/web/js/query.js b/Plan/common/src/main/resources/assets/plan/web/js/query.js index fb60d50dc..19f13311a 100644 --- a/Plan/common/src/main/resources/assets/plan/web/js/query.js +++ b/Plan/common/src/main/resources/assets/plan/web/js/query.js @@ -232,11 +232,12 @@ function isValidDate(value) { const d = value.match( /^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/ ); + if (!d) return false; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date const parsedDay = Number(d[1]); const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December const parsedYear = Number(d[3]); - return d ? new Date(parsedYear, parsedMonth, parsedDay) : null; + return new Date(parsedYear, parsedMonth, parsedDay); } function correctDate(value) { @@ -270,11 +271,11 @@ function isValidTime(value) { } function correctTime(value) { - const d = value.match(/^(\d{2}):?(\d{2})$/); + const d = value.match(/^(0\d|\d{2}):?(0\d|\d{2})$/); if (!d) return value; - let hour = d[1]; + let hour = Number(d[1]); while (hour > 23) hour--; - let minute = d[2]; + let minute = Number(d[2]); while (minute > 59) minute--; return hour + ":" + minute; } @@ -289,23 +290,49 @@ function setFilterOption( const query = id === 'view' ? filterView : filterQuery.find(function (f) { return f.id === id; }); - const element = $(`#${elementId}`); - let value = element.val(); + const element = document.getElementById(elementId); + let value = element.value; value = correctionFunction.apply(element, [value]); - element.val(value); + element.value = value; const isValid = isValidFunction.apply(element, [value]); if (isValid) { - element.removeClass("is-invalid"); - query[propertyName] = value; + element.classList.remove("is-invalid"); + query[propertyName] = value; // Updates either the query or filterView properties InvalidEntries.setAsValid(elementId); + if (id === 'view') updateViewGraph(); } else { - element.addClass("is-invalid"); + element.classList.add("is-invalid"); InvalidEntries.setAsInvalid(elementId); } } +function updateViewGraph() { + function parseTime(dateString, timeString) { + const d = dateString.match( + /^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/ + ); + const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/); + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + const parsedDay = Number(d[1]); + const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December + const parsedYear = Number(d[3]); + let hour = Number(t[1]); + let minute = Number(t[2]); + return new Date(parsedYear, parsedMonth, parsedDay, hour, minute).getTime(); + } + + const graph = graphs[0]; + + const min = parseTime(filterView.afterDate, filterView.afterTime); + const max = parseTime(filterView.beforeDate, filterView.beforeTime); + for (const axis of graph.xAxis) { + axis.setExtremes(min, max); + } +} + let query = []; function performQuery() { diff --git a/Plan/common/src/main/resources/assets/plan/web/query.html b/Plan/common/src/main/resources/assets/plan/web/query.html index 369da3c05..b8ce8d57f 100644 --- a/Plan/common/src/main/resources/assets/plan/web/query.html +++ b/Plan/common/src/main/resources/assets/plan/web/query.html @@ -141,6 +141,8 @@ +
+
@@ -354,6 +356,52 @@ document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate); document.getElementById('viewToTimeField').setAttribute('placeholder', json.view.beforeTime); + const s = { + name: {playersOnline: 'Players Online'}, + tooltip: {zeroDecimals: {valueDecimals: 0}}, + type: {areaSpline: 'areaspline'} + }; + + const playersOnlineSeries = { + name: 'Players Online', type: 'areaspline', tooltip: {valueDecimals: 0}, + data: json.viewPoints, color: '#1E90FF', yAxis: 0 + } + + graphs.push(Highcharts.stockChart('viewChart', { + rangeSelector: { + selected: 3, + buttons: linegraphButtons + }, + yAxis: { + softMax: 2, + softMin: 0 + }, + title: {text: ''}, + plotOptions: { + areaspline: { + fillOpacity: 0.4 + } + }, + series: [playersOnlineSeries], + xAxis: { + events: { + afterSetExtremes: function (event) { + if (this) { + const afterDate = Highcharts.dateFormat('%d/%m/%Y', this.min); + const afterTime = Highcharts.dateFormat('%H:%M', this.min); + const beforeDate = Highcharts.dateFormat('%d/%m/%Y', this.max); + const beforeTime = Highcharts.dateFormat('%H:%M', this.max); + document.getElementById('viewFromDateField').value = afterDate; + document.getElementById('viewFromTimeField').value = afterTime; + document.getElementById('viewToDateField').value = beforeDate; + document.getElementById('viewToTimeField').value = beforeTime; + filterView = {afterDate, afterTime, beforeDate, beforeTime}; + } + } + } + } + })); + let filterElements = ''; for (let i = 0; i < filters.length; i++) { filterElements += createFilterSelector('#filters', i, filters[i]); diff --git a/Plan/common/src/main/resources/assets/plan/web/server.html b/Plan/common/src/main/resources/assets/plan/web/server.html index 2ca221463..666ba21f8 100644 --- a/Plan/common/src/main/resources/assets/plan/web/server.html +++ b/Plan/common/src/main/resources/assets/plan/web/server.html @@ -1315,7 +1315,7 @@ }); // HighCharts Series - var s = { + const s = { name: { playersOnline: 'Players Online', uniquePlayers: 'Unique Players',