001: /**
002: * ===========================================
003: * JFreeReport : a free Java reporting library
004: * ===========================================
005: *
006: * Project Info: http://reporting.pentaho.org/
007: *
008: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
009: *
010: * This library is free software; you can redistribute it and/or modify it under the terms
011: * of the GNU Lesser General Public License as published by the Free Software Foundation;
012: * either version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015: * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016: * See the GNU Lesser General Public License for more details.
017: *
018: * You should have received a copy of the GNU Lesser General Public License along with this
019: * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020: * Boston, MA 02111-1307, USA.
021: *
022: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023: * in the United States and other countries.]
024: *
025: * ------------
026: * PaginationStep.java
027: * ------------
028: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
029: */package org.jfree.report.layout.process;
030:
031: import org.jfree.report.layout.model.BlockRenderBox;
032: import org.jfree.report.layout.model.BreakMarkerRenderBox;
033: import org.jfree.report.layout.model.LogicalPageBox;
034: import org.jfree.report.layout.model.PageBreakPositionList;
035: import org.jfree.report.layout.model.ParagraphRenderBox;
036: import org.jfree.report.layout.model.RenderBox;
037: import org.jfree.report.layout.model.RenderLength;
038: import org.jfree.report.layout.model.RenderNode;
039: import org.jfree.report.layout.model.RenderableReplacedContent;
040: import org.jfree.report.layout.model.context.StaticBoxLayoutProperties;
041: import org.jfree.util.Log;
042:
043: /**
044: * Creation-Date: 11.04.2007, 14:23:34
045: *
046: * @author Thomas Morgner
047: */
048: public final class PaginationStep extends IterateVisualProcessStep {
049: private long shift;
050: private boolean breakPending;
051: private PageBreakPositionList breakUtility;
052: private long pageHeight;
053: private long pageEnd;
054: private Object visualState;
055: private boolean breakIndicatorEncountered;
056:
057: public PaginationStep() {
058: }
059:
060: public PaginationResult performPagebreak(
061: final LogicalPageBox pageBox) {
062: final RenderNode lastChild = pageBox.getLastChild();
063: if (lastChild != null) {
064: final long lastChildY2 = lastChild.getY()
065: + lastChild.getHeight();
066: if (lastChildY2 < pageBox.getHeight()) {
067: //ModelPrinter.print(pageBox);
068: throw new IllegalStateException(
069: "Assertation failed: Block layouting did not proceed: "
070: + lastChildY2 + " < "
071: + pageBox.getHeight());
072: }
073: }
074:
075: this .breakPending = false;
076: this .shift = 0;
077: this .pageHeight = pageBox.getPageHeight();
078: this .breakIndicatorEncountered = false;
079:
080: try {
081: final PageBreakPositionList allPreviousBreak = pageBox
082: .getAllVerticalBreaks();
083: final long[] allCurrentBreaks = pageBox
084: .getPhysicalBreaks(RenderNode.VERTICAL_AXIS);
085: final long pageOffset = pageBox.getPageOffset();
086:
087: if (allCurrentBreaks.length == 0) {
088: // No maximum height.
089: throw new IllegalStateException(
090: "No page given. This is really bad.");
091: }
092:
093: // Note: For now, we limit both the header and footer to a single physical
094: // page. This safes me a lot of trouble for now.
095:
096: final BlockRenderBox headerArea = pageBox.getHeaderArea();
097: final long headerHeight = Math.min(headerArea.getHeight(),
098: allCurrentBreaks[0]);
099: headerArea.setHeight(headerHeight);
100:
101: final long lastBreakLocal = allCurrentBreaks[allCurrentBreaks.length - 1];
102: final BlockRenderBox footerArea = pageBox.getFooterArea();
103: long footerHeight = footerArea.getHeight();
104: if (allCurrentBreaks.length > 1) {
105: final long lastPageHeight = lastBreakLocal
106: - allCurrentBreaks[allCurrentBreaks.length - 2];
107: footerHeight = Math.min(footerHeight, lastPageHeight);
108: footerArea.setHeight(footerHeight);
109: }
110:
111: // Assertation: Make sure that we do not run into a infinite loop..
112: if (headerHeight + footerHeight >= lastBreakLocal) {
113: // This is also bad. There will be no space left to print a single element.
114: throw new IllegalStateException(
115: "Header and footer consume the whole page. No space left for normal-flow.");
116: }
117:
118: final PageBreakPositionList breaks = new PageBreakPositionList(
119: allPreviousBreak, allCurrentBreaks.length + 1);
120:
121: // Then add all new breaks (but take the header and footer-size into account) ..
122: if (allCurrentBreaks.length == 1) {
123: breaks.addMajorBreak(pageOffset, headerHeight);
124: breaks.addMajorBreak(
125: (lastBreakLocal - footerHeight - headerHeight)
126: + pageOffset, headerHeight);
127: } else // more than one physical page; therefore header and footer are each on a separate canvas ..
128: {
129: breaks.addMajorBreak(pageOffset, headerHeight);
130: final int breakCount = allCurrentBreaks.length - 1;
131: for (int i = 1; i < breakCount; i++) {
132: final long aBreak = allCurrentBreaks[i];
133: breaks.addMinorBreak(pageOffset
134: + (aBreak - headerHeight));
135: }
136: breaks
137: .addMajorBreak(
138: pageOffset
139: + (lastBreakLocal
140: - headerHeight - footerHeight),
141: headerHeight);
142: }
143:
144: pageEnd = breaks.getLastMasterBreak();
145: breakUtility = breaks;
146: visualState = null;
147: // now process all the other content (excluding the header and footer area)
148: if (startBlockLevelBox(pageBox)) {
149: processBoxChilds(pageBox);
150: }
151: finishBlockLevelBox(pageBox);
152:
153: if (lastChild != null) {
154: final long lastChildY2 = lastChild.getY()
155: + lastChild.getHeight();
156: if (lastChildY2 < pageBox.getHeight()) {
157: throw new IllegalStateException(
158: "Assertation failed: Pagination violated block-constraints: "
159: + lastChildY2 + " < "
160: + pageBox.getHeight());
161: }
162: }
163:
164: final long masterBreak = breaks.getLastMasterBreak();
165: final boolean overflow = breakIndicatorEncountered
166: || pageBox.getHeight() > masterBreak;
167: final boolean nextPageContainsContent = (pageBox
168: .getHeight() > masterBreak);
169: return new PaginationResult(breaks, overflow,
170: nextPageContainsContent, visualState);
171: } finally {
172: breakUtility = null;
173: visualState = null;
174: }
175: }
176:
177: protected void processParagraphChilds(final ParagraphRenderBox box) {
178: processBoxChilds(box);
179: }
180:
181: protected boolean startBlockLevelBox(final RenderBox box) {
182: if (box instanceof BreakMarkerRenderBox) {
183: breakIndicatorEncountered = true;
184: } else {
185: breakIndicatorEncountered = false;
186: }
187:
188: if (box.isFinished() == false) {
189: if (box.isCommited()) {
190: box.setFinished(true);
191: } else {
192: final StaticBoxLayoutProperties sblp = box
193: .getStaticBoxLayoutProperties();
194: if (sblp.isAvoidPagebreakInside()
195: || sblp.getWidows() > 0
196: || sblp.getOrphans() > 0) {
197: // Check, whether this box sits on a break-position. In that case, we can call that box finished as well.
198: final long boxY = box.getY();
199: final long nextMinorBreak = breakUtility
200: .findNextBreakPosition(boxY + shift);
201: final long spaceAvailable = nextMinorBreak
202: - (boxY + shift);
203:
204: // This box sits directly on a pagebreak. No matter how much content we fill in the box, it will not move.
205: // This makes this box a finished box.
206: if (spaceAvailable == 0) {
207: box.setFinished(true);
208: }
209: } else {
210: // This box defines no constraints that would cause a shift of it later in the process. We can treat it as
211: // if it is finished already ..
212: box.setFinished(true);
213: }
214: }
215: }
216:
217: final int breakIndicator = box.getManualBreakIndicator();
218:
219: // First check the simple cases:
220: // If the box wants to break, then there's no point in waiting: Shift the box and continue.
221: final RenderLength fixedPosition = box.getBoxDefinition()
222: .getFixedPosition();
223:
224: final long fixedPositionResolved = fixedPosition.resolve(
225: pageHeight, 0);
226: if (breakIndicator == RenderBox.DIRECT_MANUAL_BREAK
227: || breakPending) {
228: // find the next major break and shift the box to this position.
229: // update the 'shift' to reflect this new change. Process the contents of this box as well, as the box may
230: // have additional breaks inside (or may overflow, or whatever ..).
231: final long boxY = box.getY();
232: final long shiftedBoxY = boxY + shift;
233: final long nextNonShiftedMajorBreak = breakUtility
234: .findNextMajorBreakPosition(shiftedBoxY);
235: final long fixedPositionOnNextPage = breakUtility
236: .computeFixedPositionInFlow(
237: nextNonShiftedMajorBreak,
238: fixedPositionResolved);
239: final long nextMajorBreak = Math.max(
240: nextNonShiftedMajorBreak, fixedPositionOnNextPage);
241: // final long nextMajorBreak = breakUtility.findNextMajorBreakPosition(shiftedBoxY) + fixedPositionResolved;
242: if (nextMajorBreak < shiftedBoxY) {
243: // This band will be outside the last pagebreak. We can only shift it normally, but there is no way
244: // that we could shift it to the final position yet.
245: box.setY(boxY + shift);
246: } else {
247: final long nextShift = nextMajorBreak - boxY;
248: final long shiftDelta = nextShift - shift;
249: box.setY(boxY + nextShift);
250: BoxShifter.extendHeight(box.getParent(), shiftDelta);
251: shift = nextShift;
252: }
253: updateStateKey(box);
254: breakPending = false;
255: return true;
256: }
257:
258: // If this box does not cross any (major or minor) break, it may need no additional shifting at all.
259: if (RenderLength.AUTO.equals(fixedPosition)) {
260: if (breakUtility.isCrossingPagebreak(box, shift) == false) {
261: // The whole box fits on the current page. No need to do anything fancy.
262: if (breakIndicator == RenderBox.NO_MANUAL_BREAK) {
263: // As neither this box nor any of the children will cause a pagebreak, we can shift them and skip the processing
264: // from here.
265: BoxShifter.shiftBox(box, shift);
266: updateStateKeyDeep(box);
267: return false;
268: }
269: if (breakIndicator == RenderBox.INDIRECT_MANUAL_BREAK) {
270: // One of the children of this box will cause a manual pagebreak. We have to dive deeper into this child.
271: // for now, we will only apply the ordinary shift.
272: final long boxY = box.getY();
273: box.setY(boxY + shift);
274: updateStateKey(box);
275: return true;
276: }
277: throw new IllegalStateException(
278: "The box contains an invalid BreakIndicator.");
279: }
280:
281: // At this point we know, that the box may cause some shifting. It crosses at least one minor or major pagebreak.
282: // Right now, we are just evaluating the next break. In a future version, we could search all possible break
283: // positions up to the next major break.
284: final long boxY = box.getY();
285:
286: final long nextMinorBreak = breakUtility
287: .findNextBreakPosition(boxY + shift);
288: final long spaceAvailable = nextMinorBreak - (boxY + shift);
289:
290: // This box sits directly on a pagebreak. This means, the page is empty, and there is no need for additional
291: // shifting.
292: if (spaceAvailable == 0) {
293: box.setY(boxY + shift);
294: updateStateKey(box);
295: return true;
296: }
297:
298: final long spaceConsumed = computeUsedBoxHeight(box);
299: if (spaceAvailable < spaceConsumed) {
300: // So we have not enough space to fullfill the layout-constraints. Be it so. Lets shift the box to the next
301: // break.
302: final long nextShift = nextMinorBreak - boxY;
303: final long shiftDelta = nextShift - shift;
304: box.setY(boxY + nextShift);
305: BoxShifter.extendHeight(box.getParent(), shiftDelta);
306: shift = nextShift;
307: updateStateKey(box);
308: return true;
309: }
310:
311: // OK, there *is* enough space available. Start the normal processing
312: box.setY(boxY + shift);
313: updateStateKey(box);
314: return true;
315: }
316:
317: // If you've come this far, this means, that your box has a fixed position defined.
318:
319: final long boxY = box.getY();
320: final long shiftedBoxPosition = boxY + shift;
321: final long fixedPositionInFlow = breakUtility
322: .computeFixedPositionInFlow(shiftedBoxPosition,
323: fixedPositionResolved);
324: if (fixedPositionInFlow < shiftedBoxPosition) {
325: // This is a invalid result, create a ordinary pagebreak and try again the next time.
326: final long nextMinorBreak = breakUtility
327: .findNextBreakPosition(shiftedBoxPosition);
328: final long spaceAvailable = nextMinorBreak
329: - (shiftedBoxPosition);
330:
331: // This box sits directly on a pagebreak. This means, the page is empty, and there is no need for additional
332: // shifting.
333: if (spaceAvailable == 0) {
334: box.setY(boxY + shift);
335: updateStateKey(box);
336: return true;
337: }
338:
339: // Perform an ordinary pagebreak here ..
340: final long nextShift = nextMinorBreak - boxY;
341: final long shiftDelta = nextShift - shift;
342: box.setY(boxY + nextShift);
343: BoxShifter.extendHeight(box.getParent(), shiftDelta);
344: shift = nextShift;
345: updateStateKey(box);
346: return true;
347: }
348:
349: // The computed break seems to be valid.
350: final long fixedPositionDelta = fixedPositionInFlow
351: - shiftedBoxPosition;
352: if (breakUtility.isCrossingPagebreakWithFixedPosition(
353: shiftedBoxPosition, box.getHeight(),
354: fixedPositionResolved) == false) {
355: // The whole box fits on the current page. However, we have to apply the shifting to move the box
356: // to its defined fixed-position.
357: if (breakIndicator == RenderBox.NO_MANUAL_BREAK) {
358: // As neither this box nor any of the children will cause a pagebreak, we can shift them and skip the processing
359: // from here.
360: BoxShifter.shiftBox(box, fixedPositionDelta);
361: BoxShifter.extendHeight(box.getParent(),
362: fixedPositionDelta);
363: updateStateKeyDeep(box);
364: return false;
365: }
366: if (breakIndicator == RenderBox.INDIRECT_MANUAL_BREAK) {
367: // One of the children of this box will cause a manual pagebreak. We have to dive deeper into this child.
368: // for now, we will only apply the ordinary shift.
369: box.setY(fixedPositionInFlow);
370: shift += fixedPositionDelta;
371: BoxShifter.extendHeight(box.getParent(),
372: fixedPositionDelta);
373: updateStateKey(box);
374: return true;
375: }
376: throw new IllegalStateException(
377: "The box contains an invalid BreakIndicator.");
378: }
379:
380: // A box with a fixed position will always be printed at this position, even if it does not seem
381: // to fit there. If we move the box, we would break the explict layout constraint 'fixed-position' in
382: // favour of an implict one ('page-break: avoid').
383:
384: final long nextMinorBreak = breakUtility
385: .findNextBreakPosition(fixedPositionInFlow);
386: final long spaceAvailable = nextMinorBreak
387: - fixedPositionInFlow;
388:
389: // This box sits directly on a pagebreak. This means, the current page is empty, and there is no need for additional
390: // shifting.
391: if (spaceAvailable == 0) {
392: shift += fixedPositionDelta;
393: box.setY(fixedPositionInFlow);
394: BoxShifter
395: .extendHeight(box.getParent(), fixedPositionDelta);
396: updateStateKey(box);
397: return true;
398: }
399:
400: final long spaceConsumed = computeUsedBoxHeight(box);
401: if (spaceAvailable < spaceConsumed) {
402: // So we have not enough space to fullfill the layout-constraints. Be it so. Lets shift the box to the next
403: // break.
404: final long nextShift = nextMinorBreak - boxY;
405: final long shiftDelta = nextShift - shift;
406: box.setY(boxY + nextShift);
407: BoxShifter.extendHeight(box.getParent(), shiftDelta);
408: shift = nextShift;
409: updateStateKey(box);
410: return true;
411: }
412:
413: // OK, there *is* enough space available. Start the normal processing
414: shift += fixedPositionDelta;
415: box.setY(fixedPositionInFlow);
416: BoxShifter.extendHeight(box.getParent(), fixedPositionDelta);
417: updateStateKey(box);
418: return true;
419: }
420:
421: private void updateStateKey(final RenderBox box) {
422: final long y = box.getY();
423: if (y < (pageEnd)) {
424: final Object stateKey = box.getStateKey();
425: if (stateKey != null) {
426: // Log.debug ("Updating state key: " + stateKey);
427: this .visualState = stateKey;
428: }
429: // else
430: // {
431: // Log.debug ("No key: " + y + " <= " + (pageOffset + pageHeight));
432: // }
433: }
434: // else
435: // {
436: // Log.debug ("Not in Range: " + y + " <= " + (pageOffset + pageHeight));
437: // }
438: }
439:
440: private boolean updateStateKeyDeep(final RenderBox box) {
441: final long y = box.getY();
442: if (y < (pageEnd)) {
443: final Object stateKey = box.getStateKey();
444: if (stateKey != null) {
445: // Log.debug ("Deep: Updating state key: " + stateKey);
446: this .visualState = stateKey;
447: return true;
448: } else {
449: RenderNode lastChild = box.getLastChild();
450: while (lastChild != null) {
451: if (lastChild instanceof RenderBox == false) {
452: lastChild = lastChild.getPrev();
453: continue;
454: }
455: final RenderBox lastBox = (RenderBox) lastChild;
456: if (updateStateKeyDeep(lastBox)) {
457: return true;
458: }
459: lastChild = lastBox.getPrev();
460: }
461: return false;
462: }
463: } else {
464: // Log.debug ("Deep: Not in Range: " + y + " <= " + (pageOffset + pageHeight));
465: return false;
466: }
467: }
468:
469: protected void processBlockLevelNode(final RenderNode node) {
470: if (node instanceof RenderableReplacedContent == false) {
471: node.setY(node.getY() + shift);
472: if (breakPending == false && node.isBreakAfter()) {
473: breakPending = true;
474: }
475: return;
476: }
477:
478: // Check, whether the replaced content will fit. If it doesnt fit, we may have to break it. As this is
479: // replaced content, we can't come up with a sane break-rule and have to assume that the content can break
480: // anyway.
481: final RenderableReplacedContent rpc = (RenderableReplacedContent) node;
482: if (rpc.isAvoidPagebreaksInside() == false) {
483: node.setY(node.getY() + shift);
484: return;
485: }
486:
487: // So we have not enough space to fullfill the layout-constraints. Be it so. Lets shift the box to the next
488: // break.
489: final long boxY = node.getY();
490: final long nextMinorBreak = breakUtility
491: .findNextBreakPosition(boxY + shift);
492: final long nextShift = nextMinorBreak - boxY;
493: final long shiftDelta = nextShift - shift;
494: node.setY(boxY + nextShift);
495: BoxShifter.extendHeight(node.getParent(), shiftDelta);
496: shift = nextShift;
497: }
498:
499: protected void finishBlockLevelBox(final RenderBox box) {
500: if (breakPending == false && box.isBreakAfter()) {
501: breakPending = true;
502: }
503: }
504:
505: protected boolean startInlineLevelBox(final RenderBox box) {
506: breakIndicatorEncountered = false;
507: BoxShifter.shiftBox(box, shift);
508: return false;
509: }
510:
511: protected void processInlineLevelNode(final RenderNode node) {
512: node.setY(node.getY() + shift);
513: }
514:
515: // At a later point, we have to do some real page-breaking here. We should check, whether the box fits, and should
516: // shift the box if it doesnt.
517: protected boolean startCanvasLevelBox(final RenderBox box) {
518: breakIndicatorEncountered = false;
519: box.setY(box.getY() + shift);
520: return true;
521: }
522:
523: protected void processCanvasLevelNode(final RenderNode node) {
524: breakIndicatorEncountered = false;
525: node.setY(node.getY() + shift);
526: }
527:
528: /**
529: * Computes the height that will be required on this page to display at least some parts of the box.
530: *
531: * @param box
532: * @return
533: */
534: private long computeUsedBoxHeight(final RenderBox box) {
535: final StaticBoxLayoutProperties sblp = box
536: .getStaticBoxLayoutProperties();
537: if (sblp.isAvoidPagebreakInside()) {
538: return box.getHeight();
539: }
540:
541: if (box instanceof BlockRenderBox == false) {
542: // Canvas and inline-boxes have no notion of lines, and therefore they cannot have orphans and widows.
543: return 0;
544: }
545:
546: final int orphans = sblp.getOrphans();
547: final int widows = sblp.getWidows();
548: if (orphans == 0 && widows == 0) {
549: // Widows and orphans will be ignored if both of them are zero.
550: return 0;
551: }
552:
553: int counter = 0;
554: RenderNode child = box.getFirstChild();
555: while (child != null && counter < orphans) {
556: counter += 1;
557: child = child.getNext();
558: }
559:
560: if (child == null) {
561: return box.getHeight();
562: }
563: final long orphanHeight = box.getY()
564: - (child.getY() + child.getHeight());
565:
566: counter = 0;
567: child = box.getLastChild();
568: while (child != null && counter < orphans) {
569: counter += 1;
570: child = child.getPrev();
571: }
572:
573: if (child == null) {
574: return box.getHeight();
575: }
576: final long widowHeight = (box.getY() + box.getHeight())
577: - (child.getY());
578:
579: // todo: Compute the height the orphans and widows consume.
580: return Math.max(orphanHeight, widowHeight);
581: }
582:
583: }
|