package nl.tudelft.lifetiles.graph.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javafx.scene.paint.Color;
import javafx.scene.paint.Stop;
/**
* The model for the minimap.
*
* @author Joren Hammudoglu
*
*/
public final class MiniMap {
/**
* The max number of deviations a score is allowed to be removed from the
* average.
*/
private static final double MAX_DEVIATION = 2.0;
/**
* The max number of scores.
*/
private static final int MAX_SCORES = 1024;
/**
* The interestingness scores per bucket.
*/
private List<Double> scores;
/**
* Construct a new minimap.
*
* @param bucketCache
* the {@link BucketCache} to generate the minimap from.
*/
public MiniMap(final BucketCache bucketCache) {
sumBuckets(bucketCache.getBuckets());
}
/**
* @param buckets
* the buckets to sum
*/
private void sumBuckets(final List<Bucket> buckets) {
scores = buckets.stream().map(bucket -> bucket.interestingness())
.collect(Collectors.toList());
reduceNumScores();
}
/**
* Keeps halfing the number of scores until small enough.
*/
private void reduceNumScores() {
while (scores.size() > MAX_SCORES) {
// not an issue since this will only loop a few times
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
List<Double> newScores = new ArrayList<>(scores.size() / 2);
for (int index = 0; index < scores.size(); index += 2) {
double newScore = scores.get(index) + scores.get(index + 1);
newScores.add(newScore);
}
scores = newScores;
}
}
/**
* Create a color from the score in [0,1].
*
* @param score
* the score
* @return the color
*/
private Color colorFromScore(final double score) {
assert score >= 0 && score <= 1;
return Color.gray(1.0 - score);
}
/**
* @param collection
* the collection to take the median of
* @return the median of the scores
*/
private static double median(final Collection<Double> collection) {
List<Double> sorted = new ArrayList<>(collection);
Collections.sort(sorted);
return sorted.get(sorted.size() / 2);
}
/**
* @return the median of absolute deviations (MAD) of the scores.
*/
private double scoreMAD() {
double median = median(scores);
List<Double> deviations = scores.stream()
.map(score -> Math.abs(score - median))
.collect(Collectors.toList());
return median(deviations);
}
/**
* Correct the scores that are bigger than the MAX_SCORE.
*
* @return the list of scores without outliers.
*/
private List<Double> correctScoreOutliers() {
double median = median(scores);
double delta = MAX_DEVIATION * scoreMAD();
double boundLeft = median - delta;
double boundRight = median + delta;
return scores.parallelStream()
.map(score -> Math.min(boundRight, Math.max(boundLeft, score)))
.collect(Collectors.toList());
}
/**
* Get the colors for buckets.
*
* @return a list of colors.
*/
private List<Color> getColors() {
List<Double> newScores = correctScoreOutliers();
double max = Collections.max(newScores);
return newScores.parallelStream()
.map(score -> colorFromScore(score / max))
.collect(Collectors.toList());
}
/**
* Get the color stops.
*
* @return an array of {@link Stop}
*/
public List<Stop> getStops() {
List<Color> colors = getColors();
int numColors = colors.size();
List<Stop> stops = new ArrayList<>();
for (int index = 0; index < colors.size(); index++) {
double correction = (double) index / (double) numColors;
double offset = (double) (index + correction) / (double) numColors;
// There is no other way but to create objects in a loop in this
// case
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
Stop stop = new Stop(offset, colors.get(index));
stops.add(stop);
}
return stops;
}
}