/*
 * 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: BalancingColumnBreakingAlgorithm.java 1427143 2012-12-31 14:46:01Z gadams $ */

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.
/** * 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}
/** {@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; } }