/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.course.highscore.manager;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.olat.core.id.Identity;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.course.highscore.model.HighScoreRankingResults;
import org.olat.course.highscore.ui.HighScoreTableEntry;
import org.olat.modules.assessment.AssessmentEntry;
import org.olat.user.UserManager;
import org.springframework.stereotype.Service;
/**
* The Class HighScoreManager.
* Initial Date: 20.08.2016 <br>
* @author fkiefer, fabian.kiefer@frentix.com, www.frentix.com
*/
@Service
public class HighScoreManager {
private static final OLog log = Tracing.createLoggerFor(HighScoreManager.class);
/**
* Sort rank by score, then by id and last alphabetically,
* determine rank of each member dependent on score,
* decide whether there is a second table or not
*/
public HighScoreRankingResults sortRankByScore (List<AssessmentEntry> assessEntries,
List<HighScoreTableEntry> allMembers, List<HighScoreTableEntry> ownIdMembers,
List<List<HighScoreTableEntry>> allPodium, List<Integer> ownIdIndices,
int tableSize, Identity ownIdentity, UserManager userManager){
HighScoreTableEntry ownTableEntry = null;
for (AssessmentEntry assessmentEntry : assessEntries) {
float score = assessmentEntry.getScore() == null ? 0f : assessmentEntry.getScore().floatValue();
HighScoreTableEntry tableEntry = new HighScoreTableEntry(score,
userManager.getUserDisplayName(assessmentEntry.getIdentity()), assessmentEntry.getIdentity());
if (tableEntry.getIdentity().equals(ownIdentity)) {
ownTableEntry = tableEntry;
}
allMembers.add(tableEntry);
}
assessEntries.clear();
//3 step comparator, sorts by score then own Identity comes first, last alphabetically
Collections.sort(allMembers, new HighscoreComparator(ownIdentity));
float buffer = -1;
int index = 0;
// int rank = 1;
double[] allScores = new double[allMembers.size()];
for (int j = 0; j < allMembers.size(); j++) {
HighScoreTableEntry member = allMembers.get(j);
if (member.getScore() < buffer){
index++;
// rank = j + 1;
}
//first three position are put in separate lists, exclude zero scorers
if (index < 3 && member.getScore() > 0) {
allPodium.get(index).add(member);
}
// finding position rank for own id
if (member.getIdentity().equals(ownIdentity)){
ownIdIndices.add(j);
}
//setting rank for each member
member.setRank(index + 1);
buffer = member.getScore();
//adding scores for histogram
allScores[j] = buffer;
}
//only getting member with own id for 2nd table
ownIdMembers.addAll(allMembers.stream()
.skip(tableSize)
.filter(a -> a.getIdentity().equals(ownIdentity))
.collect(Collectors.toList()));
if (ownIdMembers.size() > 0) {
log.audit("2nd Highscore Table established");
}
return new HighScoreRankingResults(allScores, ownTableEntry);
}
/**
* Process histogram data.
*
*/
public HighScoreRankingResults processHistogramData(double[] scores, Float lowerBorder, Float upperBorder) {
try {
long classwidth;
// determine natural min, max and thus range
double max = Math.ceil(Arrays.stream(scores).max().getAsDouble());
double min = Math.floor(Arrays.stream(scores).min().getAsDouble());
double range = max - min;
// use original scores if range is too small else convert results to fit histogram
if (range <= 20) {
classwidth = 1;
return new HighScoreRankingResults(scores, classwidth, min);
} else {
// decrease amount of possible classes to avoid overlapping of large numbers(condition) on x-axis
boolean largeNumbers = range > 100d || max >= 1000d;
int maxnumberofclasses = largeNumbers ? 12 : 20;
int minnumberofclasses = largeNumbers ? 4 : 5;
int numberofclasses = 10;
// primeRange increments range until a natural factor is found or upper/lower boundary is met
boolean primeRange = true;
// equalRangeExtend alternates between upper and lower boundary extension
int equalRangeExtend = 0;
// check if a value between 20 and 6 is a natural factor of the range
// if true use it to calculate the class width
while (primeRange) {
for (int j = maxnumberofclasses; j > minnumberofclasses; j--) {
if (range % j == 0) {
numberofclasses = j;
primeRange = false;
break;
}
}
if (!primeRange || range <= 0 || equalRangeExtend > 10E3) {
break;
} else if (min - 1 > lowerBorder && equalRangeExtend % 2 == 0) {
min -= 1;
range = max - min;
equalRangeExtend++;
} else if (max + 1 < upperBorder && equalRangeExtend % 2 == 1) {
max += 1;
range = max - min;
equalRangeExtend++;
} else {
equalRangeExtend++;
}
// allow one extension if no borders are defined
primeRange = upperBorder - lowerBorder > 0;
}
// steps can only be natural numbersĀ
classwidth = Math.round(range / numberofclasses);
// modified scores are calculated and saved
double[] allScores = new double[scores.length];
for (int i = 0; i < scores.length; i++) {
// determine n-th class to fit the current score result
double n = Math.ceil((scores[i] - min) / classwidth);
// calculate higher score to fit the class width
double newscore = min + (n * classwidth);
allScores[i] = newscore;
}
return new HighScoreRankingResults(allScores, classwidth, min);
}
} catch (Exception e) {
log.error("",e);
return new HighScoreRankingResults(new double[] {0,1,2,3}, 1L, 0D);
}
}
/**
* Calculate histogram cutvalue using results from the method (processHistogramData(double[]))
*
* @param score the score
* @return the double
*/
public double calculateHistogramCutvalue(double score, long classwidth, double min) {
if (classwidth != 0) {
// determine n-th class to fit the current score result
double n = Math.ceil((score - min) / classwidth);
// calculate higher score to fit the class width
double cutvalue = min + (n * classwidth);
return cutvalue;
} else {
return score;
}
}
/**
* The Class HighscoreComparator.
* 3 step comparator, sorts by score then own Identity comes first, last alphabetically
*/
private class HighscoreComparator implements Comparator<HighScoreTableEntry> {
private Identity ownIdentity;
public HighscoreComparator(Identity ownIdentity) {
this.ownIdentity = ownIdentity;
}
@Override
public int compare(HighScoreTableEntry a, HighScoreTableEntry b) {
// 0) catch null values
if (a == null || a.getIdentity() == null || a.getName() == null) return -1;
if (b == null || b.getIdentity() == null || b.getName() == null) return -1;
// 1) sort by score
int answer = Float.compare(b.getScore(), a.getScore());
if (answer == 0) {
// 2) own Identity comes first
if (a.getIdentity().equals(ownIdentity)) return -1;
else if (b.getIdentity().equals(ownIdentity)) return 1;
// 3) sort alphabetically
else return a.getName().compareTo(b.getName());
} else {
return answer;
}
}
}
}