org.daisy.dotify.common.splitter.SplitPointHandler Maven / Gradle / Ivy
The newest version!
package org.daisy.dotify.common.splitter;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.daisy.dotify.common.collection.SplitList;
import org.daisy.dotify.common.splitter.SplitPointSpecification.Type;
/**
* Breaks units into results. All allowed break points are supplied with the input.
*
* @author Joel Håkansson
*
* @param the type of split point units
* @param the type of data source
*/
public class SplitPointHandler> {
private final List EMPTY_LIST = Collections.emptyList();
private final SplitPointCost defaultCost = new SplitPointCost() {
@Override
public double getCost(SplitPointDataSource data, int index, int breakpoint) {
// 1. the smaller the result, the higher the cost
// 2. breakable units are always preferred over forced ones
return (data.get(index).isBreakable()?1:2)*breakpoint-index;
}
};
/**
* Splits the data at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
* @param breakPoint the split point
* @param units the data
* @param the type of split point units
* @return returns a split point result
*/
@SafeVarargs
public static SplitPoint> split(float breakPoint, T ... units) {
SplitPointHandler> splitter = new SplitPointHandler<>();
return splitter.split(breakPoint, new SplitPointDataList(units), splitter.defaultCost);
}
/**
* Splits the data at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
* @param breakPoint the split point
* @param units the data
* @param options the split options
* @param the type of split point units
* @return returns a split point result
*/
public static SplitPoint> split(float breakPoint, List units, SplitOption ... options) {
SplitPointHandler> splitter = new SplitPointHandler<>();
return splitter.split(breakPoint, new SplitPointDataList(units), splitter.defaultCost, options);
}
/**
* Splits the data at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
* @param breakPoint the split point
* @param units the data
* @param cost the cost function used when determining the optimal forced split point. In other words,
* the cost function is only used if there are no breakable units available.
* @param options the split options
* @param the type of split point units
* @return returns a split point result
*/
public static SplitPoint> split(float breakPoint, List units, SplitPointCost cost, SplitOption ... options) {
SplitPointHandler> splitter = new SplitPointHandler<>();
return splitter.split(breakPoint, new SplitPointDataList(units), cost, options);
}
/**
* Splits the data at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
*
* @param breakPoint the split point
* @param data the data to split
* @param options the split options
* @return returns a split point result
*/
public SplitPoint split(float breakPoint, U data, SplitOption ... options) {
return split(breakPoint, data, defaultCost, options);
}
/**
* Splits the data at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
*
* @param breakPoint the split point
* @param data the data to split
* @param cost the cost function used when determining the optimal forced split point. In other words,
* the cost function is only used if there are no breakable units available.
* @param options the split options
* @return returns a split point result
* @throws IllegalArgumentException if cost is null
*/
public SplitPoint split(float breakPoint, U data, SplitPointCost cost, SplitOption ... options) {
SplitPointSpecification spec = find(breakPoint, data, cost, options);
if (cost==null) {
throw new IllegalArgumentException("Null cost not allowed.");
}
if (spec.getType()==Type.EMPTY) {
// pretty simple...
return new SplitPoint<>(EMPTY_LIST, EMPTY_LIST, data.createEmpty(), EMPTY_LIST, false);
} else if (spec.getType()==Type.NONE) {
return emptyHead(data);
} else if (spec.getType()==Type.ALL) {
return finalizeBreakpoint(new SplitList<>(data.getRemaining(), EMPTY_LIST), data.createEmpty(), data.getSupplements(), false);
} else {
return makeBreakpoint(data, spec);
}
}
/**
* Splits the data according to the supplied specification. A specification can be created by using
* {@link #find(float, SplitPointDataSource, SplitPointCost, SplitOption...)} on the data source.
* No data is beyond the specified split point is produced using this method.
* Also, only one of the data producing operations is called, either
* {@link SplitPointDataSource#getRemaining()} or {@link SplitPointDataSource#split(int)}.
*
* @param spec the specification
* @param data the data
* @return returns a split point result
*/
public SplitPoint split(SplitPointSpecification spec, U data) {
if (spec.getType()==Type.EMPTY) {
// pretty simple...
return new SplitPoint<>(EMPTY_LIST, EMPTY_LIST, data.createEmpty(), EMPTY_LIST, false);
} else if (spec.getType()==Type.NONE) {
return emptyHead(data);
} else if (spec.getType()==Type.ALL) {
return finalizeBreakpoint(new SplitList<>(data.getRemaining(), EMPTY_LIST), data.createEmpty(), data.getSupplements(), false);
} else {
return makeBreakpoint(data, spec);
}
}
/**
* Finds a split point at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
*
* @param breakPoint the split point
* @param data the data to split
* @param options the split options
* @return returns a split point specification
*/
public SplitPointSpecification find(float breakPoint, U data, SplitOption ... options) {
return find(breakPoint, data, defaultCost, options);
}
/**
* Finds a split point at, or before, the supplied breakPoint according to the rules
* in the data. If force is used, rules may be broken to achieve a result.
*
* @param breakPoint the split point
* @param data the data to split
* @param cost the cost function used when determining the optimal forced split point. In other words,
* the cost function is only used if there are no breakable units available.
* @param options the split options
* @return returns a split point specification
*/
public SplitPointSpecification find(float breakPoint, U data, SplitPointCost cost, SplitOption ... options) {
SplitOptions opts = SplitOptions.parse(options);
if (cost==null) {
throw new IllegalArgumentException("Null cost not allowed.");
}
if (data.isEmpty()) {
// pretty simple...
return SplitPointSpecification.empty();
} else if (breakPoint<=0) {
return SplitPointSpecification.none();
} else if (fits(data, breakPoint, opts.useLastUnitSize)) {
return SplitPointSpecification.all();
} else {
int startPos = findCollapse(data, new SizeStep<>(breakPoint, data.getSupplements(), opts.useLastUnitSize));
// If no units are returned here it's because even the first unit doesn't fit.
// Therefore, force will not help.
if (startPos<0) {
return SplitPointSpecification.none();
} else {
return findBreakpoint(data, opts.useForce, startPos, cost, opts.trimTrailing);
}
}
}
private static class SplitOptions {
boolean useForce = false;
boolean trimTrailing = true;
boolean useLastUnitSize = true;
static SplitOptions parse(SplitOption ... opts) {
SplitOptions result = new SplitOptions();
for (SplitOption option : opts) {
if (option==StandardSplitOption.ALLOW_FORCE) {
result.useForce = true;
} else if (option==StandardSplitOption.RETAIN_TRAILING) {
result.trimTrailing = false;
} else if (option==StandardSplitOption.NO_LAST_UNIT_SIZE) {
result.useLastUnitSize = false;
} else if (option == null) {
//no-op
} else {
throw new UnsupportedOperationException("'" + option +
"' is not a recognized split option");
}
}
return result;
}
}
private SplitPoint emptyHead(U data) {
return finalizeBreakpoint(new SplitList<>(EMPTY_LIST, EMPTY_LIST), data, data.getSupplements(), false);
}
private SplitPointSpecification findBreakpoint(U data, boolean force, int startPos, SplitPointCost cost, boolean trimTrailing) {
Supplements map = data.getSupplements();
int strPos = forwardSkippable(data, startPos);
// check next unit to see if it can be removed.
if (!data.hasElementAt(strPos+1)) { // last unit?
return SplitPointSpecification.all();
} else {
return findBreakpointFromPosition(data, strPos, map, force, cost, trimTrailing);
}
}
private SplitPoint makeBreakpoint(U data, SplitPointSpecification spec) {
Supplements map = data.getSupplements();
SplitResult split = data.split(spec.getIndex());
return finalizeBreakpointFull(split, map, spec.isHard(), spec.shouldTrimTrailing());
}
private SplitPointSpecification findBreakpointFromPosition(U data, int strPos, Supplements map, boolean force, SplitPointCost cost, boolean trimTrailing) {
// back up
BreakPointScannerResult result=findBreakpointBefore(data, strPos, cost);
boolean hard = false;
int tailStart;
if (result.bestBreakable!=result.bestSplitPoint) { // no breakable found, break hard
if (force) {
hard = true;
tailStart = result.bestSplitPoint+1;
} else {
tailStart = 0;
}
} else {
tailStart = result.bestBreakable+1;
}
return new SplitPointSpecification(tailStart, hard, trimTrailing);
}
private SplitPoint finalizeBreakpointFull(SplitResult result, Supplements map, boolean hard, boolean trimTrailing) {
if (trimTrailing) {
return finalizeBreakpoint(trimTrailing(result.head()), result.tail(), map, hard);
} else {
return finalizeBreakpoint(new SplitList<>(result.head(), EMPTY_LIST), result.tail(), map, hard);
}
}
private SplitPoint finalizeBreakpoint(SplitList head, U tail, Supplements map, boolean hard) {
TrimStep trimmed = new TrimStep<>(map);
findCollapse(new SplitPointDataList(head.getFirstPart()), trimmed);
List discarded = trimmed.getDiscarded();
discarded.addAll(head.getSecondPart());
return new SplitPoint<>(trimmed.getResult(), trimmed.getSupplements(), tail, discarded, hard);
}
/**
* Trims leading skippable units in the supplied list. The result is backed by the
* original list.
*
* @param in the list to trim
* @param the type of split list
* @return the list split in two parts, one with the leading skippable units, one with
* the remainder
*/
public static SplitList trimLeading(List in) {
int i;
for (i = 0; i the type of split list
* @param the type of data source
* @return a split point, the leading skippable units are placed in {@link SplitPoint#getDiscarded()}, the
* remainder are placed in {@link SplitPoint#getTail()}
*/
public static > SplitPoint trimLeading(U in) {
return skipLeading(in, findLeading(in));
}
/**
* Skips leading units in the supplied list. The result is backed by the original data source.
* No data is beyond index is produced using this method.
* @param in the list to trim
* @param index the index of the split point
* @param the type of object
* @param the type of data source
* @return a split point, the leading units are placed in {@link SplitPoint#getDiscarded()}, the
* remainder are placed in {@link SplitPoint#getTail()}
*/
public static > SplitPoint skipLeading(U in, int index) {
SplitResult res = in.split(index);
return new SplitPoint<>(null, null, res.tail(), res.head(), false);
}
/**
* Finds leading skippable units in the supplied data source.
* @param in the data source to search
* @param the type of object
* @param the type of data source
* @return returns the index of the first non-skippable unit
*/
public static > int findLeading(U in) {
int i;
for (i = 0; in.hasElementAt(i); i++) {
if (!in.get(i).isSkippable()) {
break;
}
};
return i;
}
static T maxSize(T u1, T u2) {
return (u1.getUnitSize()>=u2.getUnitSize()?u1:u2);
}
static SplitList trimTrailing(List in) {
int i;
for (i = in.size()-1; i>=0; i--) {
if (!in.get(i).isSkippable()) {
break;
}
}
return SplitList.split(in, i+1);
}
/**
* Finds the index for the last unit that fits into the given space
* @param data
* @param impl
* @return returns the index for the last unit
*/
static > int findCollapse(U data, StepForward impl) {
int units = -1;
T maxCollapsable = null;
for (int i=0; data.hasElementAt(i); i++) {
T c = data.get(i);
units++;
if (c.isCollapsible()) {
if (maxCollapsable!=null) {
if (maxCollapsable.collapsesWith(c)) {
if (maxSize(maxCollapsable, c)==c) {
//new one is now max, add the previous to collapsed
impl.addDiscarded(maxCollapsable);
maxCollapsable = c;
} else {
//old one is max, add the new one to collapsed
impl.addDiscarded(c);
}
} else {
impl.addUnit(maxCollapsable);
maxCollapsable = c;
}
} else {
maxCollapsable = c;
}
} else {
if (maxCollapsable!=null) {
impl.addUnit(maxCollapsable);
maxCollapsable = null;
}
impl.addUnit(c);
}
if (impl.overflows(maxCollapsable)) { //time to exit
units--;
return units;
}
}
if (maxCollapsable!=null) {
impl.addUnit(maxCollapsable);
maxCollapsable = null;
}
return units;
}
static int forwardSkippable(SplitPointDataSource extends SplitPointUnit, ? extends SplitPointDataSource, ?>> data, final int pos) {
SplitPointUnit c;
int ret = pos;
if (data.hasElementAt(ret) && !(c=data.get(ret)).isBreakable()) {
ret++;
while (data.hasElementAt(ret) && (c=data.get(ret)).isSkippable()) {
if (c.isBreakable()) {
return ret;
} else {
ret++;
}
}
//have we passed last element?
if (!data.hasElementAt(ret)) {
return ret-1;
} else {
return pos;
}
} else {
return ret;
}
}
static > BreakPointScannerResult findBreakpointBefore(U data, int strPos, SplitPointCost cost) {
BreakPointScannerResult res = new BreakPointScannerResult();
res.bestBreakable = -1;
res.bestSplitPoint = strPos;
double currentCost = Double.MAX_VALUE;
double currentBreakableCost = Double.MAX_VALUE;
for (int index=0; index<=strPos; index++) {
double c = cost.getCost(data, index, strPos);
if (c> boolean fits(U data, float limit, boolean useLastUnitSize) {
return totalSize(data, limit, useLastUnitSize)<=limit;
}
/**
* If the total size is less than the limit, the size is returned, otherwise a value greater
* than or equal to the limit is returned.
*
* @param data the units
* @param limit the maximum width that is relevant to calculate
* @return returns the size
*/
static > float totalSize(U data, float limit, boolean useLastUnitSize) {
float ret = 0;
Set ids = new HashSet<>();
Supplements map = data.getSupplements();
boolean hasSupplements = false;
// we check up to the limit and beyond by one element, to make sure that we check enough units
for (int i=0; data.hasElementAt(i) && ret<=limit; i++) {
T unit = data.get(i);
List suppIds = unit.getSupplementaryIDs();
if (suppIds!=null) {
for (String id : suppIds) {
if (ids.add(id)) { //id didn't already exist in the list
T item = map.get(id);
if (item!=null) {
if (!hasSupplements) {
hasSupplements = true;
ret += map.getOverhead();
}
ret += item.getUnitSize();
}
}
}
}
//last unit?
if (useLastUnitSize && !data.hasElementAt(i+1)) {
ret += unit.getLastUnitSize();
} else {
ret += unit.getUnitSize();
}
}
return ret;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy