/*
* ARX: Powerful Data Anonymization
* Copyright 2012 - 2017 Fabian Prasser, Florian Kohlmayer and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.deidentifier.arx.aggregates;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.deidentifier.arx.DataType;
import org.deidentifier.arx.DataType.DataTypeWithRatioScale;
/**
* This class enables building hierarchies for non-categorical values by mapping them
* into given intervals.
*
* @author Fabian Prasser
* @param <T>
*/
public class HierarchyBuilderIntervalBased<T> extends HierarchyBuilderGroupingBased<T> {
/**
* This class represents an node.
*
* @author Fabian Prasser
*/
public class IndexNode implements Serializable {
/** TODO */
private static final long serialVersionUID = 5985820929677249525L;
/** Children. */
private final IndexNode[] children;
/** IsLeaf. */
private final boolean isLeaf;
/** Leafs. */
private final Interval<T>[] leafs;
/** Max is exclusive. */
private final T max;
/** Min is inclusive. */
private final T min;
/**
* Creates a new instance. Min is inclusive, max is exclusive
*
* @param min
* @param max
* @param children
*/
public IndexNode(T min, T max, IndexNode[] children) {
this.min = min;
this.max = max;
this.children = children;
this.leafs = null;
this.isLeaf = false;
}
/**
* Creates a new instance. Min is inclusive, max is exclusive
*
* @param min
* @param max
* @param leafs
*/
public IndexNode(T min, T max, Interval<T>[] leafs) {
this.min = min;
this.max = max;
this.children = null;
this.leafs = leafs;
this.isLeaf = true;
}
@Override
public String toString(){
return toString("");
}
/**
*
*
* @param prefix
* @return
*/
private String toString(String prefix){
final String INTEND = " ";
StringBuilder b = new StringBuilder();
DataType<T> type = getDataType();
if (this.isLeaf) {
b.append(prefix).append("Leafs[min=");
b.append(type.format(min)).append(", max=");
b.append(type.format(max)).append("]\n");
for (Interval<T> leaf : leafs) {
b.append(prefix).append(INTEND).append("Leaf[min=");
b.append(type.format(leaf.min)).append(", max=");
b.append(type.format(leaf.max)).append(", function=");
b.append(leaf.function).append("]\n");
}
return b.toString();
} else {
b.append(prefix).append("Inner[min=");
b.append(type.format(min)).append(", max=");
b.append(type.format(max)).append("]\n");
for (IndexNode child : children) {
b.append(child.toString(prefix+INTEND));
}
return b.toString();
}
}
}
/**
* This class represents an interval.
*
* @author Fabian Prasser
* @param <T>
*/
public static class Interval<T> extends AbstractGroup {
/** TODO */
private static final long serialVersionUID = 5985820929677249525L;
/** The function. */
private final AggregateFunction<T> function;
/** Max is exclusive. */
private final T max;
/** Min is inclusive. */
private final T min;
/** The builder. */
private final HierarchyBuilderGroupingBased<T> builder;
/** Null for normal intervals, true if <min, false if >max. */
private final Boolean lower;
/**
* Constructor for creating label for null values
*
* @param builder
*/
private Interval(HierarchyBuilderGroupingBased<T> builder) {
super(DataType.NULL_VALUE);
this.builder = builder;
this.min = null;
this.max = null;
this.function = null;
this.lower = null;
}
/**
* Constructor for creating out of bounds labels.
*
* @param builder
* @param lower
* @param value
*/
private Interval(HierarchyBuilderGroupingBased<T> builder, boolean lower, T value) {
super(lower ? "<" + ((DataType<T>)builder.getDataType()).format(value) :
">=" + ((DataType<T>)builder.getDataType()).format(value));
this.builder = builder;
this.min = null;
this.max = null;
this.function = null;
this.lower = lower;
}
/**
* Creates a new instance. Min is inclusive, max is exclusive
*
* @param builder
* @param type
* @param min
* @param max
* @param function
*/
private Interval(HierarchyBuilderGroupingBased<T> builder, DataType<T> type, T min, T max, AggregateFunction<T> function) {
super(function.aggregate(new String[]{type.format(min), type.format(max)}));
this.builder = builder;
this.min = min;
this.max = max;
this.function = function;
this.lower = null;
}
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Interval<T> other = (Interval<T>) obj;
if (max == null) {
if (other.max != null) return false;
} else if (!max.equals(other.max)) return false;
if (lower == null) {
if (other.lower != null) return false;
} else if (lower != other.lower) return false;
if (min == null) {
if (other.min != null) return false;
} else if (!min.equals(other.min)) return false;
return true;
}
/**
* @return the function
*/
public AggregateFunction<T> getFunction() {
return function;
}
/**
* @return the max (inclusive)
*/
public T getMax() {
return max;
}
/**
* @return the min (exclusive)
*/
public T getMin() {
return min;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((max == null) ? 0 : max.hashCode());
result = prime * result + ((min == null) ? 0 : min.hashCode());
result = prime * result + ((lower == null) ? 0 : lower.hashCode());
return result;
}
/**
* Is this an interval for null values
* @return
*/
public boolean isNullInterval() {
return this.lower == null && this.min == null && this.max == null;
}
/**
* Is this an interval representing values that are out of bounds
* @return
*/
public boolean isOutOfBound() {
return lower != null;
}
/**
* Is this an interval representing values that are out of the lower bound
* @return
*/
public boolean isOutOfLowerBound() {
if (this.lower == null) {
throw new IllegalStateException("You may only call this on intervals that represent values that are out of bounds");
}
return lower;
}
@Override
public String toString(){
DataType<T> type = (DataType<T>)builder.getDataType();
return "Interval[min="+type.format(min)+", max="+type.format(max)+", function="+function.toString()+"]";
}
}
/**
* For each direction, this class encapsulates three bounds. Intervals will be repeated until the
* repeat-bound is reached. The outmost intervals will than be extended to the snap-bound. Values between
* the snap-bound and the label-bound will be replaced by an out-of-bounds-label. For values larger than
* the label-bound exceptions will be raised.
*
* @author Fabian Prasser
* @param <U>
*/
public static class Range<U> implements Serializable {
/** TODO */
private static final long serialVersionUID = -5385139177770612960L;
/** Bound. */
private U repeatBound;
/** Bound. */
private U snapBound;
/** Bound. */
private U labelBound;
/**
* Creates a new instance.
*
* @param repeatBound
* @param snapBound
* @param labelBound
*/
public Range(U repeatBound, U snapBound, U labelBound) {
if (!(repeatBound == null && snapBound == null && labelBound == null)) {
if (repeatBound == null || snapBound == null || labelBound == null) {
throw new IllegalArgumentException("Value must not be null");
}
}
this.repeatBound = repeatBound;
this.snapBound = snapBound;
this.labelBound = labelBound;
}
/**
* @return the labelBound
*/
public U getLabelBound() {
return labelBound;
}
/**
* @return the repeatBound
*/
public U getRepeatBound() {
return repeatBound;
}
/**
* @return the snapBound
*/
public U getSnapBound() {
return snapBound;
}
@Override
public String toString() {
return "Range [repeat=" + repeatBound + ", snap=" +
snapBound + ", label=" + labelBound + "]";
}
/**
* @param labelBound the labelBound to set
*/
private void setLabelBound(U labelBound) {
this.labelBound = labelBound;
}
/**
* @param repeatBound the repeatBound to set
*/
private void setRepeatBound(U repeatBound) {
this.repeatBound = repeatBound;
}
/**
* @param snapBound the snapBound to set
*/
private void setSnapBound(U snapBound) {
this.snapBound = snapBound;
}
}
/** TODO: Is this parameter OK?. */
private static final int INDEX_FANOUT = 2;
/** SVUID. */
private static final long serialVersionUID = 3663874945543082808L;
/**
* Creates a new instance. Snapping is disabled. Repetition is disabled. Bound is determined dynamically.
*
* @param <T>
* @param type
* @return
*/
public static <T> HierarchyBuilderIntervalBased<T> create(DataType<T> type) {
return new HierarchyBuilderIntervalBased<T>(type);
}
/**
* Creates a new instance. Data points that are out of range are handled according to the given settings.
*
* @param <T>
* @param type
* @param lowerRange
* @param upperRange
* @return
*/
public static <T> HierarchyBuilderIntervalBased<T> create(DataType<T> type, Range<T> lowerRange, Range<T> upperRange) {
return new HierarchyBuilderIntervalBased<T>(type, lowerRange, upperRange);
}
/**
* Loads a builder specification from the given file.
*
* @param <T>
* @param file
* @return
* @throws IOException
*/
@SuppressWarnings("unchecked")
public static <T> HierarchyBuilderIntervalBased<T> create(File file) throws IOException{
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
HierarchyBuilderIntervalBased<T> result = (HierarchyBuilderIntervalBased<T>)ois.readObject();
return result;
} catch (Exception e) {
throw new IOException(e);
} finally {
if (ois != null) ois.close();
}
}
/**
* Loads a builder specification from the given file.
*
* @param <T>
* @param file
* @return
* @throws IOException
*/
public static <T> HierarchyBuilderIntervalBased<T> create(String file) throws IOException{
return create(new File(file));
}
/** Adjustment. */
private Range<T> lowerRange;
/** Adjustment. */
private Range<T> upperRange;
/** Defined intervals. */
private List<Interval<T>> intervals = new ArrayList<Interval<T>>();
/**
* Creates a new instance. Snapping is disabled. Repetition is disabled. Bound is determined dynamically.
* @param type
*/
protected HierarchyBuilderIntervalBased(DataType<T> type) {
super(Type.INTERVAL_BASED, type);
if (!(type instanceof DataTypeWithRatioScale)) {
throw new IllegalArgumentException("Data type must have a ratio scale");
}
this.lowerRange = new Range<T>(null, null, null);
this.upperRange = new Range<T>(null, null, null);
this.function = AggregateFunction.forType(type).createIntervalFunction();
}
/**
* Creates a new instance. Data points that are out of range are handled according to the given settings.
* @param type
* @param lowerRange
* @param upperRange
*/
protected HierarchyBuilderIntervalBased(DataType<T> type, Range<T> lowerRange, Range<T> upperRange) {
super(Type.INTERVAL_BASED, type);
if (!(type instanceof DataTypeWithRatioScale)) {
throw new IllegalArgumentException("Data type must have a ratio scale");
}
this.lowerRange = lowerRange;
this.upperRange = upperRange;
this.function = AggregateFunction.forType(type).createIntervalFunction();
}
/**
* Adds an interval. Min is inclusive, max is exclusive. Uses the predefined default aggregate function
* @param min
* @param max
* @return
*/
public HierarchyBuilderIntervalBased<T> addInterval(T min, T max) {
if (this.getDefaultFunction() == null) {
throw new IllegalStateException("No default aggregate function defined");
}
checkInterval(getDataType(), min, max);
this.intervals.add(new Interval<T>(this, getDataType(), min, max, this.getDefaultFunction()));
this.setPrepared(false);
return this;
}
/**
* Adds an interval. Min is inclusive, max is exclusive
* @param min
* @param max
* @param function
* @return
*/
public HierarchyBuilderIntervalBased<T> addInterval(T min, T max, AggregateFunction<T> function) {
if (function==null) {
throw new IllegalArgumentException("Function must not be null");
}
checkInterval(getDataType(), min, max);
this.intervals.add(new Interval<T>(this, getDataType(), min, max, function));
this.setPrepared(false);
return this;
}
/**
* Adds an interval. Min is inclusive, max is exclusive. Interval is labeled
* with the given string
* @param min
* @param max
* @param label
* @return
*/
public HierarchyBuilderIntervalBased<T> addInterval(T min, T max, String label) {
if (label==null) {
throw new IllegalArgumentException("Label must not be null");
}
checkInterval(getDataType(), min, max);
this.intervals.add(new Interval<T>(this, getDataType(), min, max,
AggregateFunction.forType(getDataType()).createConstantFunction(label)));
this.setPrepared(false);
return this;
}
/**
* Adds an interval. Min is inclusive, max is exclusive. Uses the predefined default aggregate function
*
* @return
*/
public HierarchyBuilderIntervalBased<T> clearIntervals() {
this.intervals.clear();
this.setPrepared(false);
return this;
}
/**
* Returns all currently defined intervals.
*
* @return
*/
@SuppressWarnings("unchecked")
public List<Interval<T>> getIntervals(){
return (List<Interval<T>>)((ArrayList<T>)this.intervals).clone();
}
/**
* Returns the lower range.
*
* @return
*/
public Range<T> getLowerRange() {
return this.lowerRange;
}
/**
* Returns the upper range.
*
* @return
*/
public Range<T> getUpperRange() {
return this.upperRange;
}
@Override
@SuppressWarnings("unchecked")
public String isValid() {
String superIsValid = super.isValid();
if (superIsValid != null) return superIsValid;
if (intervals.isEmpty()) {
return "No intervals specified";
}
for (int i=1; i<intervals.size(); i++){
Interval<T> interval1 = intervals.get(i-1);
Interval<T> interval2 = intervals.get(i);
if (!interval1.getMax().equals(interval2.getMin())) {
return "Gap between " + interval1 + " and " + interval2;
}
if (interval1.getMin().equals(interval2.getMin()) &&
interval1.getMax().equals(interval2.getMax())) {
return "Repeating intervals " + interval1 + " and " + interval2;
}
}
DataTypeWithRatioScale<T> type = (DataTypeWithRatioScale<T>)getDataType();
if (lowerRange.getRepeatBound() != null &&
upperRange.getRepeatBound() != null &&
type.compare(lowerRange.getRepeatBound(), upperRange.getRepeatBound()) > 0){
return "Lower repeat bound must be < upper repeat bound";
}
if (lowerRange.getSnapBound() != null &&
lowerRange.getRepeatBound() != null &&
type.compare(lowerRange.getSnapBound(), lowerRange.getRepeatBound()) > 0){
return "Lower snap bound must be <= lower repeat bound";
}
if (lowerRange.getLabelBound() != null &&
lowerRange.getSnapBound() != null &&
type.compare(lowerRange.getLabelBound(), lowerRange.getSnapBound()) > 0){
return "Lower label bound must be <= lower snap bound";
}
if (upperRange.getRepeatBound() != null &&
upperRange.getSnapBound() != null &&
type.compare(upperRange.getSnapBound(), upperRange.getRepeatBound()) < 0){
return "Upper snap bound must be >= upper repeat bound";
}
if (lowerRange.getLabelBound() != null &&
upperRange.getSnapBound() != null &&
type.compare(upperRange.getLabelBound(), upperRange.getSnapBound()) < 0){
return "Upper label bound must be >= upper snap bound";
}
return null;
}
/**
* Checks the interval.
*
* @param <U>
* @param type
* @param min
* @param max
*/
private <U> void checkInterval(DataType<U> type, U min, U max){
int cmp = 0;
try {
cmp = type.compare(type.format(min), type.format(max));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid data item "+min+" or "+max);
}
if (cmp >= 0) throw new IllegalArgumentException("Min ("+min+") must be lower than max ("+max+")");
}
/**
* Returns the according group from the cache.
*
* @param cache
* @param interval
* @return
*/
private AbstractGroup getGroup(Map<AbstractGroup, AbstractGroup> cache, Interval<T> interval) {
AbstractGroup cached = cache.get(interval);
if (cached != null) {
return cached;
} else {
cache.put(interval, interval);
return interval;
}
}
/**
* Returns the matching interval.
*
* @param index
* @param type
* @param tValue
* @return
*/
@SuppressWarnings("unchecked")
private Interval<T> getInterval(IndexNode index, DataTypeWithRatioScale<T> type, T tValue) {
// Find interval
int shift = (int)Math.floor(type.ratio(type.subtract(tValue, index.min), type.subtract(index.max, index.min)));
T offset = type.multiply(type.subtract(index.max, index.min), shift);
Interval<T> interval = getInterval(index, type.subtract(tValue, offset));
// Check
if (interval == null) { throw new IllegalStateException("No interval found for: " + type.format(tValue)); }
// Create first result interval
T lower = type.add(interval.min, offset);
T upper = type.add(interval.max, offset);
return new Interval<T>(this, (DataType<T>)type, lower, upper, interval.function);
}
/**
* Performs the index lookup.
*
* @param node
* @param value
* @return
*/
private Interval<T> getInterval(IndexNode node, T value) {
@SuppressWarnings("unchecked")
DataTypeWithRatioScale<T> type = (DataTypeWithRatioScale<T>)getDataType();
if (node.isLeaf) {
for (Interval<T> leaf : node.leafs) {
if (type.compare(leaf.min, value) <= 0 && type.compare(leaf.max, value) > 0) {
return leaf;
}
}
} else {
for (IndexNode child : node.children) {
if (type.compare(child.min, value) <= 0 && type.compare(child.max, value) > 0) {
return getInterval(child, value);
}
}
}
throw new IllegalStateException("No interval found for: "+type.format(value));
}
/**
* Returns the matching interval.
*
* @param index
* @param type
* @param tValue
* @return
*/
@SuppressWarnings("unchecked")
private Interval<T> getIntervalUpperSnap(IndexNode index, DataTypeWithRatioScale<T> type, T tValue) {
// Find interval
double shift = Math.floor(type.ratio(type.subtract(tValue, index.min), type.subtract(index.max, index.min)));
T offset = type.multiply(type.subtract(index.max, index.min), shift);
T value = type.subtract(tValue, offset);
Interval<T> interval = null;
for (int j=0; j<intervals.size(); j++) {
Interval<T> i = intervals.get(j);
if (type.compare(i.min, value) <= 0 &&
type.compare(i.max, value) > 0) {
// If on lower bound, use next-lower interval
if (type.compare(value, i.min) == 0) {
if (j>0) {
// Simply use the next one
interval = intervals.get(j-1);
break;
} else {
// Wrap around
interval = intervals.get(intervals.size()-1);
offset = type.multiply(type.subtract(index.max, index.min), shift-1);
break;
}
} else {
interval = i;
break;
}
}
}
if (interval == null && intervals.size()==1){
interval = intervals.get(0);
}
// Check
if (interval == null) {
throw new IllegalStateException("Internal error. Sorry for that!");
}
// Create first result interval
T lower = type.add(interval.min, offset);
T upper = type.add(interval.max, offset);
return new Interval<T>(this, (DataType<T>)type, lower, upper, interval.function);
}
/**
* Adds an interval.
*
* @param interval
*/
protected void addInterval(Interval<T> interval) {
this.intervals.add(interval);
}
/**
* Returns adjusted ranges.
*
* @return Array containing {lower, upper}
*/
@SuppressWarnings("unchecked")
protected Range<T>[] getAdjustedRanges() {
// Create adjustments
Range<T> tempLower = new Range<T>(null, null, null);
Range<T> tempUpper = new Range<T>(null, null, null);
if (lowerRange.getRepeatBound() != null) {
tempLower.setRepeatBound(lowerRange.getRepeatBound());
} else {
tempLower.setRepeatBound(intervals.get(0).min);
}
if (lowerRange.getSnapBound() != null) {
tempLower.setSnapBound(lowerRange.getSnapBound());
} else {
tempLower.setSnapBound(tempLower.getRepeatBound());
}
if (lowerRange.getLabelBound() != null) {
tempLower.setLabelBound(lowerRange.getLabelBound());
} else {
tempLower.setLabelBound(tempLower.getSnapBound());
}
if (upperRange.getRepeatBound() != null) {
tempUpper.setRepeatBound(upperRange.getRepeatBound());
} else {
tempUpper.setRepeatBound(intervals.get(intervals.size()-1).max);
}
if (upperRange.getSnapBound() != null) {
tempUpper.setSnapBound(upperRange.getSnapBound());
} else {
tempUpper.setSnapBound(tempUpper.getRepeatBound());
}
if (upperRange.getLabelBound() != null) {
tempUpper.setLabelBound(upperRange.getLabelBound());
} else {
tempUpper.setLabelBound(tempUpper.getSnapBound());
}
return new Range[]{tempLower, tempUpper};
}
@Override
@SuppressWarnings("unchecked")
protected AbstractGroup[][] prepareGroups() {
// Check
String valid = isValid();
if (valid != null) {
throw new IllegalArgumentException(valid);
}
// Create adjustments
Range<T>[] ranges = getAdjustedRanges();
Range<T> tempLower = ranges[0];
Range<T> tempUpper = ranges[1];
// Build leaf level index
ArrayList<IndexNode> nodes = new ArrayList<IndexNode>();
for (int i=0, len = intervals.size(); i < len; i+=INDEX_FANOUT) {
int min = i;
int max = Math.min(i+INDEX_FANOUT-1, len-1);
List<Interval<T>> leafs = new ArrayList<Interval<T>>();
for (int j=min; j<=max; j++) {
leafs.add(intervals.get(j));
}
nodes.add(new IndexNode(intervals.get(min).min,
intervals.get(max).max,
leafs.toArray(new Interval[leafs.size()])));
}
// Builder inner nodes
while (nodes.size()>1) {
List<IndexNode> current = (List<IndexNode>)nodes.clone();
nodes.clear();
for (int i=0, len = current.size(); i < len; i+=INDEX_FANOUT) {
int min = i;
int max = Math.min(i+INDEX_FANOUT-1, len-1);
List<IndexNode> temp = new ArrayList<IndexNode>();
for (int j=min; j<=max; j++) {
temp.add(current.get(j));
}
nodes.add(new IndexNode(current.get(min).min,
current.get(max).max,
temp.toArray(new HierarchyBuilderIntervalBased.IndexNode[temp.size()])));
}
}
// Prepare
String[] data = getData();
List<AbstractGroup[]> result = new ArrayList<AbstractGroup[]>();
IndexNode index = nodes.get(0);
// Prepare
DataTypeWithRatioScale<T> type = (DataTypeWithRatioScale<T>)getDataType();
Map<AbstractGroup, AbstractGroup> cache = new HashMap<AbstractGroup, AbstractGroup>();
// Create snap intervals
Interval<T> lowerSnap = getInterval(index, type, tempLower.repeatBound);
lowerSnap = new Interval<T>(this, getDataType(), tempLower.snapBound, lowerSnap.max, lowerSnap.function);
Interval<T> upperSnap = getIntervalUpperSnap(index, type, tempUpper.repeatBound);
upperSnap = new Interval<T>(this, getDataType(), upperSnap.min, tempUpper.snapBound, upperSnap.function);
// Overlapping snaps -> one interval
if (type.compare(lowerSnap.max, upperSnap.min)>0) {
// We could use lowerSnap.function or upperSnap.function
lowerSnap = new Interval<T>(this, getDataType(), lowerSnap.min, upperSnap.max, lowerSnap.function);
upperSnap = lowerSnap;
}
// Create first column
AbstractGroup[] first = new AbstractGroup[data.length];
for (int i=0; i<data.length; i++){
T value = type.parse(data[i]);
Interval<T> interval;
if (value == null) {
interval = new Interval<T>(this);
} else if (type.compare(value, tempLower.labelBound) < 0) {
throw new IllegalArgumentException(type.format(value)+ " is < lower label bound");
} else if (type.compare(value, tempLower.snapBound) < 0) {
interval = new Interval<T>(this, true, tempLower.snapBound);
} else if (type.compare(value, tempUpper.labelBound) >= 0) {
throw new IllegalArgumentException(type.format(value)+ " is >= upper label bound");
} else if (type.compare(value, tempUpper.snapBound) >= 0) {
interval = new Interval<T>(this, false, tempUpper.snapBound);
} else {
interval = getInterval(index, type, value);
}
if (interval.min != null && interval.max != null){
if (type.compare(interval.min, lowerSnap.max) < 0){
interval = lowerSnap;
} else if (type.compare(interval.max, upperSnap.min) > 0){
interval = upperSnap;
}
}
first[i] = getGroup(cache, interval);
}
result.add(first);
// Clean
index = null;
// Create other columns
List<Group<T>> groups = new ArrayList<Group<T>>();
if (!super.getLevels().isEmpty()) groups = super.getLevels().get(0).getGroups();
if (cache.size()>1 && !groups.isEmpty()) {
// Prepare
List<Interval<T>> newIntervals = new ArrayList<Interval<T>>();
int intervalIndex = 0;
int multiplier = 0;
T width = type.subtract(intervals.get(intervals.size() - 1).max, intervals.get(0).min);
// Merge intervals
for (Group<T> group : groups) {
// Find min and max
T min = null;
T max = null;
for (int i = 0; i < group.getSize(); i++) {
Interval<T> current = intervals.get(intervalIndex++);
T offset = type.multiply(width, multiplier);
T cMin = type.add(current.min, offset);
T cMax = type.add(current.max, offset);
if (min == null || type.compare(min, cMin) > 0) {
min = cMin;
}
if (max == null || type.compare(max, cMax) < 0) {
max = cMax;
}
if (intervalIndex == intervals.size()) {
intervalIndex = 0;
multiplier++;
}
}
// Add interval
newIntervals.add(new Interval<T>(this, getDataType(), min, max, group.getFunction()));
}
// Compute next column
HierarchyBuilderIntervalBased<T> builder = new HierarchyBuilderIntervalBased<T>(getDataType(),
tempLower,
tempUpper);
for (Interval<T> interval : newIntervals) {
builder.addInterval(interval.min, interval.max, interval.function);
}
for (int i=1; i<super.getLevels().size(); i++){
for (Group<T> sgroup : super.getLevel(i).getGroups()) {
builder.getLevel(i-1).addGroup(sgroup.getSize(), sgroup.getFunction());
}
}
// Copy data
builder.prepare(data);
AbstractGroup[][] columns = builder.getPreparedGroups();
for (AbstractGroup[] column : columns) {
result.add(column);
}
} else {
if (cache.size()>1) {
AbstractGroup[] column = new AbstractGroup[data.length];
@SuppressWarnings("serial")
AbstractGroup element = new AbstractGroup(DataType.ANY_VALUE) {};
for (int i = 0; i < column.length; i++) {
column[i] = element;
}
result.add(column);
}
}
// Return
return result.toArray(new AbstractGroup[0][0]);
}
/**
* Sets the data array.
*
* @param data
*/
protected void setData(String[] data){
super.setData(data);
}
/**
* Sets the groups on higher levels of the hierarchy.
*
* @param levels
*/
protected void setLevels(List<Level<T>> levels) {
super.setLevels(levels);
}
}