package polly.rx.core; import java.awt.Color; import java.io.OutputStream; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import polly.rx.MSG; import polly.rx.entities.ScoreBoardEntry; import polly.rx.graphs.HighlightArea; import polly.rx.graphs.ImageGraph; import polly.rx.graphs.NamedPoint; import polly.rx.graphs.Point; import polly.rx.graphs.Point.PointType; import polly.rx.graphs.PointSet; import polly.rx.graphs.YScale; import de.skuzzle.polly.sdk.PersistenceManagerV2; import de.skuzzle.polly.sdk.PersistenceManagerV2.Atomic; import de.skuzzle.polly.sdk.PersistenceManagerV2.Param; import de.skuzzle.polly.sdk.PersistenceManagerV2.Read; import de.skuzzle.polly.sdk.PersistenceManagerV2.Write; import de.skuzzle.polly.sdk.exceptions.DatabaseException; import de.skuzzle.polly.sdk.time.DateUtils; import de.skuzzle.polly.sdk.time.Time; public class ScoreBoardManager { private final static Color[] COLORS = { Color.RED, Color.BLUE, Color.BLACK, Color.GREEN, Color.PINK }; public final static NumberFormat NUMBER_FORMAT = DecimalFormat.getInstance( Locale.ENGLISH); static { ((DecimalFormat) NUMBER_FORMAT).applyPattern("0.00"); //$NON-NLS-1$ } private final static DateFormat getDateFormat() { return new SimpleDateFormat("dd.MM.yyyy - HH:mm"); //$NON-NLS-1$ } private final static int AVERAGE_ELEMENTS = 10; private PersistenceManagerV2 persistence; public ScoreBoardManager(PersistenceManagerV2 persistence) { this.persistence = persistence; } public int maxColors() { return COLORS.length; } public OutputStream createLatestGraph(List<ScoreBoardEntry> all, int maxMonths, Collection<NamedPoint> allPoints) { if (all.isEmpty()) { return null; } Collections.sort(all, ScoreBoardEntry.BY_DATE); final ImageGraph g = new ImageGraph(850, 500); g.setxLabels(this.createXLabels(maxMonths)); g.setDrawGridVertical(true); final PointSet points = new PointSet(Color.RED); points.setConnect(true); points.setStrength(2f); final PointSet rank = new PointSet(new Color(0, 0, 255, 128)); rank.setConnect(true); rank.setStrength(2f); final PointSet averagePoints = new PointSet(Color.RED); averagePoints.setConnect(true); averagePoints.setName(MSG.bind(MSG.scoreboardAvgPoints, AVERAGE_ELEMENTS)); final PointSet averageRank = new PointSet(new Color(0, 0, 255, 128)); averageRank.setConnect(true); averageRank.setName(MSG.bind(MSG.scoreboardAvgRank, AVERAGE_ELEMENTS)); final Point lowest = this.createPointSet(all, maxMonths, points, rank, averagePoints, averageRank); YScale pointScale = points.calculateScale(MSG.scoreboardPoints, 10); if (pointScale == null) { pointScale = new YScale(MSG.scoreboardPoints, 2000, 30000, 2000); } YScale rankScale = rank.calculateScale(MSG.scoreboardRank, 10); if (rankScale == null) { rankScale = new YScale(MSG.scoreboardRank, 0, 1000, 10); } points.setScale(pointScale); averagePoints.setScale(pointScale); rank.setScale(rankScale); averageRank.setScale(rankScale); points.setName(MSG.scoreboardPoints); g.addPointSet(points); g.addPointSet(rank); // g.addPointSet(averagePoints); // g.addPointSet(averageRank); g.setLeftScale(pointScale); g.setRightScale(rankScale); g.getLeftScale().setDrawGrid(true); if (lowest != null) { g.addHighlightArea(new HighlightArea("", 0, lowest.getX(), //$NON-NLS-1$ new Color(0, 0, 0, 20))); } g.updateImage(); allPoints.addAll(g.getRawPointsFromLastDraw()); return g.getBytes(); } public OutputStream createMultiGraph(int maxMonths, Collection<NamedPoint> allPoints, String...names) { ImageGraph g = new ImageGraph(850, 500); g.setxLabels(this.createXLabels(maxMonths)); g.setDrawGridVertical(true); final PointSet common = new PointSet(); // just for calculating common scale int max = Math.min(COLORS.length, names.length); for (int i = 0; i < max; ++i) { final List<ScoreBoardEntry> entries = this.getEntries(names[i]); Collections.sort(entries, ScoreBoardEntry.BY_DATE); final Color next = COLORS[i]; final PointSet left = new PointSet(next); left.setConnect(true); this.createPointSet(entries, maxMonths, left, new PointSet(), new PointSet(), new PointSet()); common.addAll(left); g.addPointSet(left); } YScale pointScale = common.calculateScale(MSG.scoreboardPoints, 10); if (pointScale == null) { pointScale = new YScale(MSG.scoreboardPoints, 6000, 30000, 2000); } pointScale.setDrawGrid(true); for (final PointSet ps : g.getData()) { ps.setScale(pointScale); } g.setLeftScale(pointScale); g.updateImage(); allPoints.addAll(g.getRawPointsFromLastDraw()); return g.getBytes(); } private final String[] createXLabels(int maxMonths) { final DateFormat df = new SimpleDateFormat("MMM yyyy"); //$NON-NLS-1$ String[] labels = new String[maxMonths]; final Date today = Time.currentTime(); for (int i = 0; i < maxMonths; ++i) { final Calendar c = Calendar.getInstance(); c.setTime(today); c.add(Calendar.MONTH, -(maxMonths - (i + 2))); labels[i] = df.format(c.getTime()); } return labels; } private Point createPointSet(List<ScoreBoardEntry> entries, int maxMonths, PointSet left, PointSet right, PointSet averagePoints, PointSet averageRank) { if (entries.size() < 2) { return null; } final DateFormat df = new SimpleDateFormat("dd.MM.yyyy"); //$NON-NLS-1$ final ScoreBoardEntry oldest = entries.get(0); final Date today = Time.currentTime(); Point greatestLowerZeroPoints = null; Point lowestGreaterZeroPoints = null; Point greatestLowerZeroRank = null; Point lowestGreaterZeroRank = null; final ArrayDeque<Point> pointQueue = new ArrayDeque<>(AVERAGE_ELEMENTS); for (final ScoreBoardEntry entry : entries) { final int monthsAgo = this.getMonthsAgo(today, entry.getDate(), maxMonths); final double x = this.calcX(entry.getDate(), monthsAgo); final Point points = new NamedPoint( MSG.bind(MSG.scoreboardDatePoints, df.format(entry.getDate()), entry.getPoints()), x, entry.getPoints(), PointType.NONE); final Point rank = new NamedPoint( MSG.bind(MSG.scoreboardDateRank, df.format(entry.getDate()), entry.getRank()), x, entry.getRank(), PointType.NONE); if (x < 0.0 && (greatestLowerZeroPoints == null || greatestLowerZeroPoints.getX() < x)) { greatestLowerZeroPoints = points; greatestLowerZeroRank = rank; } if (x > 0.0 && (lowestGreaterZeroPoints == null || lowestGreaterZeroPoints.getX() > x)) { lowestGreaterZeroPoints = points; lowestGreaterZeroRank = rank; } if (monthsAgo <= 0) { // do not add points that are older than X_LABELS months continue; } right.add(rank); left.add(points); } if (lowestGreaterZeroPoints != null && greatestLowerZeroPoints != null && Math.abs( DateUtils.monthsBetween(today, oldest.getDate())) > maxMonths) { // interpolate correct y-axis intersection for points double m = (lowestGreaterZeroPoints.getY() - greatestLowerZeroPoints.getY()) / (lowestGreaterZeroPoints.getX() - greatestLowerZeroPoints.getX()); double y = m * (-lowestGreaterZeroPoints.getX()) + lowestGreaterZeroPoints.getY(); final Point zero = new Point(0.0, y, PointType.DOT); left.add(zero); m = (lowestGreaterZeroRank.getY() - greatestLowerZeroRank.getY()) / (lowestGreaterZeroRank.getX() - greatestLowerZeroRank.getX()); y = m * (-lowestGreaterZeroRank.getX()) + lowestGreaterZeroRank.getY(); right.add(new Point(0, y, PointType.DOT)); } for (final Point p : left) { if (pointQueue.size() == AVERAGE_ELEMENTS) { Point avg = this.calcAverage(pointQueue, pointQueue.size()); averagePoints.add(avg); pointQueue.poll(); } pointQueue.add(p); } pointQueue.clear(); for (final Point p : right) { if (pointQueue.size() == AVERAGE_ELEMENTS) { Point avg = this.calcAverage(pointQueue, pointQueue.size()); averageRank.add(avg); pointQueue.poll(); } pointQueue.add(p); } left.setName(oldest.getVenadName()); right.setName(MSG.scoreboardRank); return lowestGreaterZeroPoints; } private Point calcAverage(Iterable<Point> iterable, int size) { double xsum = 0; double ysum = 0; for (Point p : iterable) { xsum = p.getX(); ysum += p.getY(); } return new Point(xsum, ysum / size, PointType.NONE); } private int getMonthsAgo(Date today, Date other, int maxMonths) { final int monthsBetween = DateUtils.monthsBetween(today, other); final int monthsAgo = maxMonths - monthsBetween - 1; return monthsAgo; } private double calcX(Date d, int monthsAgo) { final Calendar c = Calendar.getInstance(); c.setTime(d); final int dayInMonth = c.get(Calendar.DAY_OF_MONTH); final int days = c.getActualMaximum(Calendar.DAY_OF_MONTH); final double x = (monthsAgo - 1) + (double) dayInMonth / (double) days; return x; } public static class EntryResult { public final int previousRank; public final int previousPoints; public final int currentRank; public final int currentPoints; public final String previousDate; public final String venad; public EntryResult(String venad, int previousRank, int previousPoints, int currentRank, int currentPoints, Date previousDate) { this.venad = venad; this.previousRank = previousRank; this.previousPoints = previousPoints; this.currentRank = currentRank; this.currentPoints = currentPoints; if (previousDate == null) { this.previousDate = ""; //$NON-NLS-1$ } else { this.previousDate = getDateFormat().format(previousDate); } } } public synchronized List<EntryResult> addEntries( final Collection<ScoreBoardEntry> entries) throws DatabaseException { final List<EntryResult> result = new ArrayList<>(50); final Atomic op = new Atomic() { @Override public void perform(Write write) throws DatabaseException { final Read read = write.read(); for (final ScoreBoardEntry entry : entries) { // find existing entries for this user. HINT: newest is first final List<ScoreBoardEntry> existing = read.findList( ScoreBoardEntry.class, ScoreBoardEntry.SBE_BY_USER, new Param(entry.getVenadName())); int previousRank = -1; int previousPoints = -1; Date prevDate = null; boolean skip = false; if (!existing.isEmpty()) { final ScoreBoardEntry latest = existing.get(existing.size() - 1); previousRank = latest.getRank(); previousPoints = latest.getPoints(); prevDate = latest.getDate(); skip = latest.getPoints() == entry.getPoints() && latest.getRank() == entry.getRank() && DateUtils.isSameDay(latest.getDate(), entry.getDate()); } // for result final EntryResult er = new EntryResult( entry.getVenadName(), previousRank, previousPoints, entry.getRank(), entry.getPoints(), prevDate); result.add(er); if (!skip) { write.single(entry); } } } }; this.persistence.writeAtomic(op); return result; } public void addEntry(final ScoreBoardEntry entry) throws DatabaseException { Collection<ScoreBoardEntry> existing = this.getEntries(entry.getVenadName()); // skip identical entries. for (ScoreBoardEntry e : existing) { if (DateUtils.isSameDay(e.getDate(), entry.getDate())) { return; } } this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) { write.single(entry); } }); } public void deleteEntry(final int id) throws DatabaseException { this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) throws DatabaseException { final ScoreBoardEntry sbe = write.read().find(ScoreBoardEntry.class, id); if (sbe == null) { return; } write.remove(sbe); } }); } public List<ScoreBoardEntry> getEntries(String venad) { return this.persistence.atomic().findList(ScoreBoardEntry.class, ScoreBoardEntry.SBE_BY_USER, new Param(venad)); } public List<ScoreBoardEntry> getEntries() { return this.persistence.atomic().findList(ScoreBoardEntry.class, ScoreBoardEntry.ALL_SBE_DISTINCT); } }