/*
* Copyright 2004-2005 The Apache Software Foundation.
*
* 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.
*/
/* $Id: BreakingAlgorithm.java 357002 2005-12-15 10:57:45Z jeremias $ */
package org.apache.fop.layoutmgr;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.fo.FONode;
import org.apache.fop.traits.MinOptMax;
/**
* The set of nodes is sorted into lines indexed into activeLines.
* The nodes in each line are linked together in a single linked list by the
* KnuthNode.next field. The activeLines array contains a link to the head of
* the linked list in index 'line*2' and a link to the tail at index 'line*2+1'.
* <p>
* The set of active nodes can be traversed by
* <pre>
* for (int line = startLine; line < endLine; line++) {
* for (KnuthNode node = getNode(line); node != null; node = node.next) {
* // Do something with 'node'
* }
* }
* </pre>
*/
public abstract class BreakingAlgorithm {
/** the logger */
protected static Log log = LogFactory.getLog(BreakingAlgorithm.class);
protected static final int INFINITE_RATIO = 1000;
private static final int MAX_RECOVERY_ATTEMPTS = 50;
// constants identifying a subset of the feasible breaks
public static final int ALL_BREAKS = 0; // all feasible breaks are ok
public static final int NO_FLAGGED_PENALTIES = 1; // this forbids hyphenation
public static final int ONLY_FORCED_BREAKS = 2; // wrap-option = "no-wrap"
// parameters of Knuth's algorithm:
// penalty value for flagged penalties
private int flaggedPenalty = 50;
// demerit for consecutive lines ending at flagged penalties
protected int repeatedFlaggedDemerit = 50;
// demerit for consecutive lines belonging to incompatible fitness classes
protected int incompatibleFitnessDemerit = 50;
// maximum number of consecutive lines ending with a flagged penalty
// only a value >= 1 is a significant limit
protected int maxFlaggedPenaltiesCount;
/**
* The threshold for considering breaks to be acceptable.
*/
private double threshold;
/**
* The paragraph of KnuthElements.
*/
protected KnuthSequence par;
/**
* The width of a line (or height of a column in page-breaking mode).
* -1 indicates that the line widths are different for each line.
*/
protected int lineWidth = -1;
private boolean force = false;
/** If set to true, doesn't ignore break possibilities which are definitely too short. */
protected boolean considerTooShort = false;
protected KnuthNode lastDeactivatedNode = null;
private KnuthNode lastTooLong;
private KnuthNode lastTooShort;
private KnuthNode lastDeactivated;
protected int alignment;
protected int alignmentLast;
protected boolean bFirst;
/**
* The set of active nodes.
*/
protected KnuthNode[] activeLines;
/**
* The number of active nodes.
*/
protected int activeNodeCount;
/**
* The lowest available line in the set of active nodes.
*/
protected int startLine = 0;
/**
* The highest + 1 available line in the set of active nodes.
*/
protected int endLine = 0;
/**
* The total width of all elements handled so far.
*/
protected int totalWidth;
/**
* The total stretch of all elements handled so far.
*/
protected int totalStretch = 0;
/**
* The total shrink of all elements handled so far.
*/
protected int totalShrink = 0;
protected BestRecords best;
private KnuthNode[] positions;
/** @see isPartOverflowRecoveryActivated() */
private boolean partOverflowRecoveryActivated = true;
public BreakingAlgorithm(int align, int alignLast,
boolean first, boolean partOverflowRecovery,
int maxFlagCount) {
alignment = align;
alignmentLast = alignLast;
bFirst = first;
this.partOverflowRecoveryActivated = partOverflowRecovery;
this.best = new BestRecords();
maxFlaggedPenaltiesCount = maxFlagCount;
}
// this class represent a feasible breaking point
public class KnuthNode {
/** index of the breakpoint represented by this node */
public int position;
/** number of the line ending at this breakpoint */
public int line;
/** fitness class of the line ending at his breakpoint */
public int fitness;
/** accumulated width of the KnuthElements */
public int totalWidth;
/** accumulated stretchability of the KnuthElements */
public int totalStretch;
/** accumulated shrinkability of the KnuthElements */
public int totalShrink;
/** adjustment ratio if the line ends at this breakpoint */
public double adjustRatio;
/** available stretch of the line ending at this breakpoint */
public int availableShrink;
/** available shrink of the line ending at this breakpoint */
public int availableStretch;
/** difference between target and actual line width */
public int difference;
/** minimum total demerits up to this breakpoint */
public double totalDemerits;
/** best node for the preceding breakpoint */
public KnuthNode previous;
/** next possible node in the same line */
public KnuthNode next;
/**
* Holds the number of subsequent recovery attempty that are made to get content fit
* into a line.
*/
public int fitRecoveryCounter = 0;
public KnuthNode(int position, int line, int fitness,
int totalWidth, int totalStretch, int totalShrink,
double adjustRatio, int availableShrink, int availableStretch, int difference,
double totalDemerits, KnuthNode previous) {
this.position = position;
this.line = line;
this.fitness = fitness;
this.totalWidth = totalWidth;
this.totalStretch = totalStretch;
this.totalShrink = totalShrink;
this.adjustRatio = adjustRatio;
this.availableShrink = availableShrink;
this.availableStretch = availableStretch;
this.difference = difference;
this.totalDemerits = totalDemerits;
this.previous = previous;
}
public String toString() {
return "<KnuthNode at " + position + " " +
totalWidth + "+" + totalStretch + "-" + totalShrink +
" line:" + line +
" prev:" + (previous != null ? previous.position : -1) +
" dem:" + totalDemerits +
">";
}
}
// this class stores information about how the nodes
// which could start a line
// ending at the current element
protected class BestRecords {
private static final double INFINITE_DEMERITS = Double.POSITIVE_INFINITY;
//private static final double INFINITE_DEMERITS = 1E11;
private double bestDemerits[] = new double[4];
private KnuthNode bestNode[] = new KnuthNode[4];
private double bestAdjust[] = new double[4];
private int bestDifference[] = new int[4];
private int bestAvailableShrink[] = new int[4];
private int bestAvailableStretch[] = new int[4];
private int bestIndex = -1;
public BestRecords() {
reset();
}
public void addRecord(double demerits, KnuthNode node, double adjust,
int availableShrink, int availableStretch,
int difference, int fitness) {
if (demerits > bestDemerits[fitness]) {
log.error("New demerits value greater than the old one");
}
bestDemerits[fitness] = demerits;
bestNode[fitness] = node;
bestAdjust[fitness] = adjust;
bestAvailableShrink[fitness] = availableShrink;
bestAvailableStretch[fitness] = availableStretch;
bestDifference[fitness] = difference;
if (bestIndex == -1 || demerits < bestDemerits[bestIndex]) {
bestIndex = fitness;
}
}
public boolean hasRecords() {
return (bestIndex != -1);
}
public boolean notInfiniteDemerits(int fitness) {
return (bestDemerits[fitness] != INFINITE_DEMERITS);
}
public double getDemerits(int fitness) {
return bestDemerits[fitness];
}
public KnuthNode getNode(int fitness) {
return bestNode[fitness];
}
public double getAdjust(int fitness) {
return bestAdjust[fitness];
}
public int getAvailableShrink(int fitness) {
return bestAvailableShrink[fitness];
}
public int getAvailableStretch(int fitness) {
return bestAvailableStretch[fitness];
}
public int getDifference(int fitness) {
return bestDifference[fitness];
}
public double getMinDemerits() {
if (bestIndex != -1) {
return getDemerits(bestIndex);
} else {
// anyway, this should never happen
return INFINITE_DEMERITS;
}
}
public void reset() {
for (int i = 0; i < 4; i ++) {
bestDemerits[i] = INFINITE_DEMERITS;
// there is no need to reset the other arrays
}
bestIndex = -1;
}
}
/**
* @return the number of times the algorithm should try to move overflowing content to the
* next line/page.
*/
protected int getMaxRecoveryAttempts() {
return MAX_RECOVERY_ATTEMPTS;
}
/**
* Controls the behaviour of the algorithm in cases where the first element of a part
* overflows a line/page.
* @return true if the algorithm should try to send the element to the next line/page.
*/
protected boolean isPartOverflowRecoveryActivated() {
return this.partOverflowRecoveryActivated;
}
public abstract void updateData1(int total, double demerits) ;
public abstract void updateData2(KnuthNode bestActiveNode,
KnuthSequence sequence,
int total) ;
public void setConstantLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
}
public int findBreakingPoints(KnuthSequence par, /*int lineWidth,*/
double threshold, boolean force,
int allowedBreaks) {
return findBreakingPoints(par, 0, threshold, force, allowedBreaks);
}
public int findBreakingPoints(KnuthSequence par, int startIndex,
/*int lineWidth,*/
double threshold, boolean force,
int allowedBreaks) {
this.par = par;
this.threshold = threshold;
this.force = force;
//this.lineWidth = lineWidth;
initialize();
activeLines = new KnuthNode[20];
// reset lastTooShort and lastTooLong, as they could be not null
// because of previous calls to findBreakingPoints
lastTooShort = lastTooLong = null;
// reset startLine and endLine
startLine = endLine = 0;
// current element in the paragraph
KnuthElement thisElement = null;
// previous element in the paragraph is a KnuthBox?
boolean previousIsBox = false;
// index of the first KnuthBox in the sequence
int firstBoxIndex = startIndex;
while (alignment != org.apache.fop.fo.Constants.EN_CENTER
&& ! ((KnuthElement) par.get(firstBoxIndex)).isBox()) {
firstBoxIndex++;
}
// create an active node representing the starting point
activeLines = new KnuthNode[20];
addNode(0, createNode(firstBoxIndex, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, null));
if (log.isTraceEnabled()) {
log.trace("Looping over " + (par.size() - startIndex) + " elements");
}
KnuthNode lastForced = getNode(0);
// main loop
for (int i = startIndex; i < par.size(); i++) {
thisElement = getElement(i);
if (thisElement.isBox()) {
// a KnuthBox object is not a legal line break
totalWidth += thisElement.getW();
previousIsBox = true;
handleBox((KnuthBox) thisElement);
} else if (thisElement.isGlue()) {
// a KnuthGlue object is a legal line break
// only if the previous object is a KnuthBox
// consider these glues according to the value of allowedBreaks
if (previousIsBox
&& !(allowedBreaks == ONLY_FORCED_BREAKS)) {
considerLegalBreak(thisElement, i);
}
totalWidth += thisElement.getW();
totalStretch += thisElement.getY();
totalShrink += thisElement.getZ();
previousIsBox = false;
} else {
// a KnuthPenalty is a legal line break
// only if its penalty is not infinite;
// consider all penalties, non-flagged penalties or non-forcing penalties
// according to the value of allowedBreaks
if (((KnuthPenalty) thisElement).getP() < KnuthElement.INFINITE
&& (!(allowedBreaks == NO_FLAGGED_PENALTIES) || !(((KnuthPenalty) thisElement).isFlagged()))
&& (!(allowedBreaks == ONLY_FORCED_BREAKS) || ((KnuthPenalty) thisElement).getP() == -KnuthElement.INFINITE)) {
considerLegalBreak(thisElement, i);
}
previousIsBox = false;
}
if (activeNodeCount == 0) {
if (!force) {
log.debug("Could not find a set of breaking points " + threshold);
return 0;
}
if (lastTooShort == null || lastForced.position == lastTooShort.position) {
if (isPartOverflowRecoveryActivated()) {
// content would overflow, insert empty line/page and try again
KnuthNode node = createNode(
lastTooLong.previous.position, lastTooLong.previous.line + 1, 1,
0, 0, 0,
0, 0, 0,
0, 0, lastTooLong.previous);
lastForced = node;
node.fitRecoveryCounter = lastTooLong.previous.fitRecoveryCounter + 1;
log.debug("first part doesn't fit into line, recovering: "
+ node.fitRecoveryCounter);
if (node.fitRecoveryCounter > getMaxRecoveryAttempts()) {
FONode contextFO = findContextFO(par, node.position + 1);
throw new RuntimeException(FONode.decorateWithContextInfo(
"Some content could not fit "
+ "into a line/page after " + getMaxRecoveryAttempts()
+ " attempts. Giving up to avoid an endless loop.", contextFO));
}
} else {
lastForced = lastTooLong;
}
} else {
lastForced = lastTooShort;
}
log.debug("Restarting at node " + lastForced);
i = restartFrom(lastForced, i);
}
}
finish();
if (log.isTraceEnabled()) {
log.trace("Main loop completed " + activeNodeCount);
log.trace("Active nodes=" + toString(""));
}
// there is at least one set of breaking points
// select one or more active nodes, removing the others from the list
int line = filterActiveNodes();
// for each active node, create a set of breaking points
for (int i = startLine; i < endLine; i++) {
for (KnuthNode node = getNode(i); node != null; node = node.next) {
updateData1(node.line, node.totalDemerits);
calculateBreakPoints(node, par, node.line);
}
}
activeLines = null;
return line;
}
/**
* This method tries to find the context FO for a position in a KnuthSequence.
* @param seq the KnuthSequence to inspect
* @param position the index of the position in the KnuthSequence
* @return the requested context FO note or null, if no context node could be determined
*/
private FONode findContextFO(KnuthSequence seq, int position) {
KnuthElement el = seq.getElement(position);
while (el.getLayoutManager() == null && position < seq.size() - 1) {
position++;
el = seq.getElement(position);
}
Position pos = (el != null ? el.getPosition() : null);
LayoutManager lm = (pos != null ? pos.getLM() : null);
while (pos instanceof NonLeafPosition) {
pos = ((NonLeafPosition)pos).getPosition();
if (pos != null && pos.getLM() != null) {
lm = pos.getLM();
}
}
if (lm != null) {
return lm.getFObj();
} else {
return null;
}
}
protected void initialize() {
this.totalWidth = 0;
this.totalStretch = 0;
this.totalShrink = 0;
}
protected KnuthNode createNode(int position, int line, int fitness,
int totalWidth, int totalStretch, int totalShrink,
double adjustRatio, int availableShrink, int availableStretch, int difference,
double totalDemerits, KnuthNode previous) {
return new KnuthNode(position, line, fitness,
totalWidth, totalStretch, totalShrink,
adjustRatio, availableShrink, availableStretch,
difference, totalDemerits, previous);
}
protected KnuthNode createNode(int position, int line, int fitness,
int totalWidth, int totalStretch, int totalShrink) {
return new KnuthNode(position, line, fitness,
totalWidth, totalStretch, totalShrink,
best.getAdjust(fitness), best.getAvailableShrink(fitness), best.getAvailableStretch(fitness),
best.getDifference(fitness), best.getDemerits(fitness), best.getNode(fitness));
}
protected void handleBox(KnuthBox box) {
}
protected int restartFrom(KnuthNode restartingNode, int currentIndex) {
restartingNode.totalDemerits = 0;
addNode(restartingNode.line, restartingNode);
startLine = restartingNode.line;
endLine = startLine + 1;
totalWidth = restartingNode.totalWidth;
totalStretch = restartingNode.totalStretch;
totalShrink = restartingNode.totalShrink;
lastTooShort = lastTooLong = null;
// the width, stretch and shrink already include the width,
// stretch and shrink of the suppressed glues;
// advance in the sequence in order to avoid taking into account
// these elements twice
int restartingIndex = restartingNode.position;
while (restartingIndex + 1 < par.size()
&& !(getElement(restartingIndex + 1).isBox())) {
restartingIndex ++;
}
return restartingIndex;
}
protected void considerLegalBreak(KnuthElement element, int elementIdx) {
if (log.isTraceEnabled()) {
log.trace("Feasible breakpoint at " + par.indexOf(element) + " " + totalWidth + "+" + totalStretch + "-" + totalShrink);
log.trace("\tCurrent active node list: " + activeNodeCount + " " + this.toString("\t"));
}
lastDeactivated = null;
lastTooLong = null;
for (int line = startLine; line < endLine; line++) {
for (KnuthNode node = getNode(line); node != null; node = node.next) {
if (node.position == elementIdx) {
continue;
}
int difference = computeDifference(node, element, elementIdx);
double r = computeAdjustmentRatio(node, difference);
int availableShrink = totalShrink - node.totalShrink;
int availableStretch = totalStretch - node.totalStretch;
if (log.isTraceEnabled()) {
log.trace("\tr=" + r + " difference=" + difference);
log.trace("\tline=" + line);
}
// The line would be too long.
if (r < -1 || element.isForcedBreak()) {
// Deactivate node.
if (log.isTraceEnabled()) {
log.trace("Removing " + node);
}
removeNode(line, node);
lastDeactivated = compareNodes(lastDeactivated, node);
}
// The line is within the available shrink and the threshold.
if (r >= -1 && r <= threshold) {
int fitnessClass = computeFitness(r);
double demerits = computeDemerits(node, element, fitnessClass, r);
if (log.isTraceEnabled()) {
log.trace("\tDemerits=" + demerits);
log.trace("\tFitness class=" + fitnessClass);
}
if (demerits < best.getDemerits(fitnessClass)) {
// updates best demerits data
best.addRecord(demerits, node, r, availableShrink, availableStretch,
difference, fitnessClass);
lastTooShort = null;
}
}
// The line is way too short, but we are in forcing mode, so a node is
// calculated and stored in lastValidNode.
if (force && (r <= -1 || r > threshold)) {
int fitnessClass = computeFitness(r);
double demerits = computeDemerits(node, element, fitnessClass, r);
int newWidth = totalWidth;
int newStretch = totalStretch;
int newShrink = totalShrink;
// add the width, stretch and shrink of glue elements after
// the break
// this does not affect the dimension of the line / page, only
// the values stored in the node; these would be as if the break
// was just before the next box element, thus ignoring glues and
// penalties between the "real" break and the following box
for (int i = elementIdx; i < par.size(); i++) {
KnuthElement tempElement = getElement(i);
if (tempElement.isBox()) {
break;
} else if (tempElement.isGlue()) {
newWidth += tempElement.getW();
newStretch += tempElement.getY();
newShrink += tempElement.getZ();
} else if (tempElement.isForcedBreak() && i != elementIdx) {
break;
}
}
if (r <= -1) {
if (lastTooLong == null || demerits < lastTooLong.totalDemerits) {
lastTooLong = createNode(elementIdx, line + 1, fitnessClass,
newWidth, newStretch, newShrink,
r, availableShrink, availableStretch,
difference, demerits, node);
if (log.isTraceEnabled()) {
log.trace("Picking tooLong " + lastTooLong);
}
}
} else {
if (lastTooShort == null || demerits <= lastTooShort.totalDemerits) {
if (considerTooShort) {
//consider possibilities which are too short
best.addRecord(demerits, node, r,
availableShrink, availableStretch,
difference, fitnessClass);
}
lastTooShort = createNode(elementIdx, line + 1, fitnessClass,
newWidth, newStretch, newShrink,
r, availableShrink, availableStretch,
difference, demerits, node);
if (log.isTraceEnabled()) {
log.trace("Picking tooShort " + lastTooShort);
}
}
}
}
}
addBreaks(line, elementIdx);
}
}
private void addBreaks(int line, int elementIdx) {
if (!best.hasRecords()) {
return;
}
int newWidth = totalWidth;
int newStretch = totalStretch;
int newShrink = totalShrink;
// add the width, stretch and shrink of glue elements after
// the break
// this does not affect the dimension of the line / page, only
// the values stored in the node; these would be as if the break
// was just before the next box element, thus ignoring glues and
// penalties between the "real" break and the following box
for (int i = elementIdx; i < par.size(); i++) {
KnuthElement tempElement = getElement(i);
if (tempElement.isBox()) {
break;
} else if (tempElement.isGlue()) {
newWidth += tempElement.getW();
newStretch += tempElement.getY();
newShrink += tempElement.getZ();
} else if (tempElement.isForcedBreak() && i != elementIdx) {
break;
}
}
// add nodes to the active nodes list
double minimumDemerits = best.getMinDemerits() + incompatibleFitnessDemerit;
for (int i = 0; i <= 3; i++) {
if (best.notInfiniteDemerits(i) && best.getDemerits(i) <= minimumDemerits) {
// the nodes in activeList must be ordered
// by line number and position;
if (log.isTraceEnabled()) {
log.trace("\tInsert new break in list of " + activeNodeCount);
}
KnuthNode newNode = createNode(elementIdx, line + 1, i,
newWidth, newStretch, newShrink);
addNode(line + 1, newNode);
}
}
best.reset();
}
/**
* Return the difference between the line width and the width of the break that
* ends in 'element'.
* @param activeNode
* @param element
* @return The difference in width. Positive numbers mean extra space in the line,
* negative number that the line overflows.
*/
protected int computeDifference(KnuthNode activeNode, KnuthElement element,
int elementIndex) {
// compute the adjustment ratio
int actualWidth = totalWidth - activeNode.totalWidth;
if (element.isPenalty()) {
actualWidth += element.getW();
}
return getLineWidth() - actualWidth;
}
/**
* Return the adjust ration needed to make up for the difference. A ration of
* <ul>
* <li>0 means that the break has the exact right width</li>
* <li>>= -1 && < 0 means that the break is to wider than the line,
* but within the minimim values of the glues.</li>
* <li>>0 && < 1 means that the break is smaller than the line width,
* but within the maximum values of the glues.</li>
* <li>> 1 means that the break is too small to make up for the glues.</li>
* </ul>
* @param activeNode
* @param difference
* @return The ration.
*/
protected double computeAdjustmentRatio(KnuthNode activeNode, int difference) {
// compute the adjustment ratio
if (difference > 0) {
int maxAdjustment = totalStretch - activeNode.totalStretch;
if (maxAdjustment > 0) {
return (double) difference / maxAdjustment;
} else {
return INFINITE_RATIO;
}
} else if (difference < 0) {
int maxAdjustment = totalShrink - activeNode.totalShrink;
if (maxAdjustment > 0) {
return (double) difference / maxAdjustment;
} else {
return -INFINITE_RATIO;
}
} else {
return 0;
}
}
/**
* <p>Figure out the fitness class of this line (tight, loose,
* very tight or very loose).
* </p>
* <p>See the section on "More Bells and Whistles" in Knuth's
* "Breaking Paragraphs Into Lines".
* </p>
* @param r
* @return
*/
private int computeFitness(double r) {
if (r < -0.5) {
return 0;
} else if (r <= 0.5) {
return 1;
} else if (r <= 1) {
return 2;
} else {
return 3;
}
}
protected double computeDemerits(KnuthNode activeNode, KnuthElement element,
int fitnessClass, double r) {
double demerits = 0;
// compute demerits
double f = Math.abs(r);
f = 1 + 100 * f * f * f;
if (element.isPenalty() && element.getP() >= 0) {
f += element.getP();
demerits = f * f;
} else if (element.isPenalty() && !element.isForcedBreak()) {
double penalty = element.getP();
demerits = f * f - penalty * penalty;
} else {
demerits = f * f;
}
if (element.isPenalty() && ((KnuthPenalty) element).isFlagged()
&& getElement(activeNode.position).isPenalty()
&& ((KnuthPenalty) getElement(activeNode.position)).isFlagged()) {
// add demerit for consecutive breaks at flagged penalties
demerits += repeatedFlaggedDemerit;
// there are at least two consecutive lines ending with a flagged penalty;
// check if the previous line end with a flagged penalty too,
// and if this situation is allowed
int flaggedPenaltiesCount = 2;
for (KnuthNode prevNode = activeNode.previous;
prevNode != null && flaggedPenaltiesCount <= maxFlaggedPenaltiesCount;
prevNode = prevNode.previous) {
KnuthElement prevElement = getElement(prevNode.position);
if (prevElement.isPenalty()
&& ((KnuthPenalty) prevElement).isFlagged()) {
// the previous line ends with a flagged penalty too
flaggedPenaltiesCount ++;
} else {
// the previous line does not end with a flagged penalty,
// exit the loop
break;
}
}
if (maxFlaggedPenaltiesCount >= 1
&& flaggedPenaltiesCount > maxFlaggedPenaltiesCount) {
// add infinite demerits, so this break will not be chosen
// unless there isn't any alternative break
demerits += BestRecords.INFINITE_DEMERITS;
}
}
if (Math.abs(fitnessClass - activeNode.fitness) > 1) {
// add demerit for consecutive breaks
// with very different fitness classes
demerits += incompatibleFitnessDemerit;
}
demerits += activeNode.totalDemerits;
return demerits;
}
protected void finish() {
}
/**
* Return the element at index idx in the paragraph.
* @param idx index of the element.
* @return the element at index idx in the paragraph.
*/
protected KnuthElement getElement(int idx) {
return (KnuthElement) par.get(idx);
}
/**
* Compare two KnuthNodes and return the node with the least demerit.
* @param node1 The first knuth node.
* @param node2 The other knuth node.
* @return the node with the least demerit.
*/
protected KnuthNode compareNodes(KnuthNode node1, KnuthNode node2) {
if (node1 == null || node2.position > node1.position) {
return node2;
}
if (node2.position == node1.position) {
if (node2.totalDemerits < node1.totalDemerits) {
return node2;
}
}
return node1;
}
/**
* Add a KnuthNode at the end of line 'line'.
* If this is the first node in the line, adjust endLine accordingly.
* @param line
* @param node
*/
protected void addNode(int line, KnuthNode node) {
int headIdx = line * 2;
if (headIdx >= activeLines.length) {
KnuthNode[] oldList = activeLines;
activeLines = new KnuthNode[headIdx + headIdx];
System.arraycopy(oldList, 0, activeLines, 0, oldList.length);
}
node.next = null;
if (activeLines[headIdx + 1] != null) {
activeLines[headIdx + 1].next = node;
} else {
activeLines[headIdx] = node;
endLine = line+1;
}
activeLines[headIdx + 1] = node;
activeNodeCount++;
}
/**
* Remove the first node in line 'line'. If the line then becomes empty, adjust the
* startLine accordingly.
* @param line
* @param node
*/
protected void removeNode(int line, KnuthNode node) {
KnuthNode n = getNode(line);
if (n != node) {
log.error("Should be first");
} else {
activeLines[line*2] = node.next;
if (node.next == null) {
activeLines[line*2+1] = null;
}
while (startLine < endLine && getNode(startLine) == null) {
startLine++;
}
}
activeNodeCount--;
}
protected KnuthNode getNode(int line) {
return activeLines[line * 2];
}
/**
* Return true if the position 'idx' is a legal breakpoint.
* @param idx
* @return
*/
private boolean isLegalBreakpoint(int idx) {
KnuthElement elm = getElement(idx);
if (elm.isPenalty() && elm.getP() != KnuthElement.INFINITE) {
return true;
} else if (idx > 0 && elm.isGlue() && getElement(idx-1).isBox()) {
return true;
} else {
return false;
}
}
public int getDifference(int line) {
return positions[line].difference;
}
public double getAdjustRatio(int line) {
return positions[line].adjustRatio;
}
public int getStart(int line) {
KnuthNode previous = positions[line].previous;
return line == 0 ? 0 : previous.position + 1;
}
public int getEnd(int line) {
return positions[line].position;
}
protected int getLineWidth(int line) {
if (this.lineWidth < 0) {
throw new IllegalStateException("lineWidth must be set"
+ (this.lineWidth != 0 ? " and positive, but it is: " + this.lineWidth : ""));
} else {
return this.lineWidth;
}
}
protected int getLineWidth() {
return this.lineWidth;
}
/**
* Return a string representation of a MinOptMax in the form of a
* "width+stretch-shrink". Useful only for debugging.
* @param mom
* @return
*/
private static String width(MinOptMax mom) {
return mom.opt + "+" + (mom.max - mom.opt) + "-" + (mom.opt - mom.min);
}
public String toString(String prepend) {
StringBuffer sb = new StringBuffer();
sb.append("[\n");
for (int i = startLine; i < endLine; i++) {
for (KnuthNode node = getNode(i); node != null; node = node.next) {
sb.append(prepend + "\t" + node + ",\n");
}
}
sb.append(prepend + "]");
return sb.toString();
}
protected abstract int filterActiveNodes() ;
private void calculateBreakPoints(KnuthNode node, KnuthSequence par,
int total) {
KnuthNode bestActiveNode = node;
// use bestActiveNode to determine the optimum breakpoints
for (int i = node.line; i > 0; i--) {
updateData2(bestActiveNode, par, total);
bestActiveNode = bestActiveNode.previous;
}
}
}