blob: 06c3c538d37be1529b40de7941f9f2492ef8d183 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id$ */
package org.apache.fop.layoutmgr;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.fop.traits.MinOptMax;
/**
* This is a the breaking algorithm that is responsible for balancing columns in multi-column
* layout.
*/
public class BalancingColumnBreakingAlgorithm extends PageBreakingAlgorithm {
private int columnCount;
private List<Integer> idealBreaks;
public BalancingColumnBreakingAlgorithm(LayoutManager topLevelLM,
PageProvider pageProvider,
PageBreakingLayoutListener layoutListener,
int alignment, int alignmentLast,
MinOptMax footnoteSeparatorLength,
boolean partOverflowRecovery,
int columnCount) {
super(topLevelLM, pageProvider, layoutListener,
alignment, alignmentLast,
footnoteSeparatorLength, partOverflowRecovery, false, false);
this.columnCount = columnCount;
this.considerTooShort = true; //This is important!
}
/** {@inheritDoc} */
protected double computeDemerits(KnuthNode activeNode,
KnuthElement element, int fitnessClass, double r) {
double demerits = Double.MAX_VALUE;
if (idealBreaks == null) {
idealBreaks = calculateIdealBreaks(activeNode.position);
}
LinkedList<Integer> curPossibility = getPossibilityTrail(activeNode);
boolean notIdeal = false;
int idealDemerit = columnCount + 1 - curPossibility.size();
if (curPossibility.size() > idealBreaks.size()) {
return demerits;
}
for (int breakPos = 0; breakPos < curPossibility.size(); breakPos++) {
if (curPossibility.get(breakPos) != 0
&& !curPossibility.get(breakPos).equals(idealBreaks.get(breakPos))) {
notIdeal = true;
break;
}
}
if (!notIdeal) {
demerits = idealDemerit;
}
return demerits;
}
private List<Integer> calculateIdealBreaks(int startPos) {
List<ColumnContent> previousPreviousBreaks = null;
List<ColumnContent> previousBreaks = null;
List<ColumnContent> breaks = new ArrayList<ColumnContent>();
breaks.add(new ColumnContent(startPos, par.size() - 1));
do {
previousPreviousBreaks = previousBreaks;
previousBreaks = breaks;
breaks = getInitialBreaks(startPos, getAverageColumnLength(breaks));
} while (!breaks.equals(previousBreaks) && !breaks.equals(previousPreviousBreaks));
breaks = sortElementsForBreaks(breaks);
return getElementIdBreaks(breaks, startPos);
}
private static final class ColumnContent {
public final int startIndex;
public final int endIndex;
ColumnContent(int startIndex, int endIndex) {
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public int hashCode() {
return startIndex << 16 | endIndex;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColumnContent)) {
return false;
} else {
ColumnContent other = (ColumnContent) obj;
return other.startIndex == startIndex && other.endIndex == endIndex;
}
}
@Override
public String toString() {
return startIndex + "-" + endIndex;
}
}
private int getAverageColumnLength(List<ColumnContent> columns) {
int totalLength = 0;
for (ColumnContent col : columns) {
totalLength += calcContentLength(par, col.startIndex, col.endIndex);
}
return totalLength / columnCount;
}
private List<ColumnContent> getInitialBreaks(int startIndex, int averageColLength) {
List<ColumnContent> initialColumns = new ArrayList<ColumnContent>();
int colStartIndex = startIndex;
int totalLength = 0;
int idealBreakLength = averageColLength;
int previousBreakLength = 0;
int prevBreakIndex = startIndex;
boolean prevIsBox = false;
int colNumber = 1;
for (int i = startIndex; i < par.size(); i++) {
KnuthElement element = (KnuthElement) par.get(i);
if (isLegalBreak(i, prevIsBox)) {
int breakLength = totalLength
+ (element instanceof KnuthPenalty ? element.getWidth() : 0);
if (breakLength > idealBreakLength && colNumber < columnCount) {
int breakIndex;
if (breakLength - idealBreakLength > idealBreakLength - previousBreakLength) {
breakIndex = prevBreakIndex;
totalLength = previousBreakLength;
} else {
breakIndex = element instanceof KnuthPenalty ? i : i - 1;
totalLength = breakLength;
}
initialColumns.add(new ColumnContent(colStartIndex, breakIndex));
i = getNextStartIndex(breakIndex);
colStartIndex = i--;
colNumber++;
idealBreakLength += averageColLength;
} else {
previousBreakLength = breakLength;
prevBreakIndex = element instanceof KnuthPenalty ? i : i - 1;
prevIsBox = false;
}
} else {
totalLength += element instanceof KnuthPenalty ? 0 : element.getWidth();
prevIsBox = element instanceof KnuthBox;
}
}
assert initialColumns.size() == columnCount - 1;
initialColumns.add(new ColumnContent(colStartIndex, par.size() - 1));
return initialColumns;
}
private int getNextStartIndex(int breakIndex) {
int startIndex = breakIndex;
@SuppressWarnings("unchecked")
Iterator<KnuthElement> iter = par.listIterator(breakIndex);
while (iter.hasNext() && !(iter.next() instanceof KnuthBox)) {
startIndex++;
}
return startIndex;
}
private List<ColumnContent> sortElementsForBreaks(List<ColumnContent> breaks) {
boolean changes;
/* Relax factor to make balancing more visually appealing as in some cases
* strict balancing would lead to ragged column endings. */
int fFactor = 4000;
do {
changes = false;
ColumnContent curColumn = breaks.get(breaks.size() - 1);
int curColLength = calcContentLength(par, curColumn.startIndex, curColumn.endIndex);
for (int colIndex = (breaks.size() - 1); colIndex > 0; colIndex--) {
ColumnContent prevColumn = breaks.get(colIndex - 1);
int prevColLength = calcContentLength(par, prevColumn.startIndex, prevColumn.endIndex);
if (prevColLength < curColLength) {
int newBreakIndex = curColumn.startIndex;
boolean prevIsBox = true;
while (newBreakIndex <= curColumn.endIndex && !(isLegalBreak(newBreakIndex, prevIsBox))) {
newBreakIndex++;
prevIsBox = par.get(newBreakIndex) instanceof KnuthBox;
}
if (newBreakIndex < curColumn.endIndex) {
if (prevIsBox) {
newBreakIndex--;
}
int newStartIndex = getNextStartIndex(newBreakIndex);
int newPrevColLength = calcContentLength(par, prevColumn.startIndex, newBreakIndex);
if (newPrevColLength <= fFactor + curColLength) {
prevColumn = new ColumnContent(prevColumn.startIndex, newBreakIndex);
breaks.set(colIndex - 1, prevColumn);
breaks.set(colIndex, new ColumnContent(newStartIndex, curColumn.endIndex));
prevColLength = calcContentLength(par, prevColumn.startIndex, newBreakIndex);
changes = true;
}
}
}
curColLength = prevColLength;
curColumn = prevColumn;
}
} while (changes);
return breaks;
}
private boolean isLegalBreak(int index, boolean prevIsBox) {
KnuthElement element = (KnuthElement) par.get(index);
return element instanceof KnuthPenalty && element.getPenalty() < KnuthPenalty.INFINITE
|| prevIsBox && element instanceof KnuthGlue;
}
private int calcContentLength(KnuthSequence par, int startIndex, int endIndex) {
return ElementListUtils.calcContentLength(par, startIndex, endIndex) + getPenaltyWidth(endIndex);
}
private int getPenaltyWidth(int index) {
KnuthElement element = (KnuthElement) par.get(index);
return element instanceof KnuthPenalty ? element.getWidth() : 0;
}
private List<Integer> getElementIdBreaks(List<ColumnContent> breaks, int startPos) {
List<Integer> elementIdBreaks = new ArrayList<Integer>();
elementIdBreaks.add(startPos);
for (ColumnContent column : breaks) {
if (breaks.get(breaks.size() - 1).equals(column)) {
continue;
}
elementIdBreaks.add(column.endIndex);
}
return elementIdBreaks;
}
private LinkedList<Integer> getPossibilityTrail(KnuthNode activeNode) {
LinkedList<Integer> trail = new LinkedList<Integer>();
KnuthNode previous = activeNode;
do {
trail.addFirst(previous.position);
previous = previous.previous;
} while (previous != null);
return trail;
}
}