001: package jimm.datavision.layout;
002:
003: import jimm.datavision.*;
004: import jimm.datavision.field.Field;
005: import jimm.datavision.field.ImageField;
006: import java.io.PrintWriter;
007: import java.util.*;
008:
009: /**
010: * A layout engine is responsible for formatting and outputting report data.
011: * <code>LayoutEngine</code> is an abstract class. The Template design
012: * pattern is heavily used to provide a framework for concrete layout
013: * engine subclasses.
014: *
015: * @author Jim Menard, <a href="mailto:jimm@io.com">jimm@io.com</a>
016: */
017: public abstract class LayoutEngine {
018:
019: /** The number of points per inch. */
020: public static final int POINTS_PER_INCH = 72;
021:
022: protected static final int SECT_REPORT_HEADER = 0;
023: protected static final int SECT_REPORT_FOOTER = 1;
024: protected static final int SECT_PAGE_HEADER = 2;
025: protected static final int SECT_PAGE_FOOTER = 3;
026: protected static final int SECT_GROUP_HEADER = 4;
027: protected static final int SECT_GROUP_FOOTER = 5;
028: protected static final int SECT_DETAIL = 6;
029:
030: /** Set by report in <code>Report.setLayoutEngine</code>. */
031: protected Report report;
032: protected int pageNumber;
033: protected PrintWriter out;
034: protected boolean newPage;
035: protected double pageHeight;
036: protected double pageWidth;
037: protected double pageHeightUsed;
038: protected Section currentSection;
039: protected boolean wantsMoreData;
040: protected int previousSectionArea;
041:
042: /**
043: * Constructor.
044: */
045: public LayoutEngine() {
046: this (null);
047: }
048:
049: /**
050: * Constructor.
051: *
052: * @param out output print writer
053: */
054: public LayoutEngine(PrintWriter out) {
055: report = null;
056: this .out = out;
057: wantsMoreData = true;
058: previousSectionArea = -1;
059: }
060:
061: public void setReport(Report r) {
062: report = r;
063: }
064:
065: /**
066: * Returns the page width in points. This value is only valid after
067: * {@link #start} has been called.
068: *
069: * @return the page width in points
070: */
071: public double pageWidth() {
072: return pageWidth;
073: }
074:
075: /**
076: * Returns the page height in points. This value is only valid after
077: * {@link #start} has been called.
078: *
079: * @return the page height in points
080: */
081: public double pageHeight() {
082: return pageHeight;
083: }
084:
085: /**
086: * Called by a report before retrieving each row of data, this method returns
087: * <code>true</code> if this layout engine wants more data. Concrete
088: * subclasses can set the <code>wantsMoreData</code> instance variable
089: * to <code>false</code> if the user cancells a report run, for example.
090: *
091: * @return <code>true</code> if this layout engine would like more data
092: */
093: public boolean wantsMoreData() {
094: return wantsMoreData;
095: }
096:
097: /**
098: * Called by someone else running the report to cancel all the hard work
099: * this layout engine has performed.
100: */
101: public void cancel() {
102: closeOutput();
103: }
104:
105: /**
106: * Called by the report at the beginning of a report run.
107: * <p>
108: * By this time, <code>report</code> will be non-<code>null</code>
109: * because we have been added to a report (which in turn sets our
110: * <code>report</code> instance variable).
111: */
112: public void start() {
113: newPage = true;
114: pageHeightUsed = 0;
115: pageNumber = 0;
116: pageHeight = report.getPaperFormat().getHeight();
117: pageWidth = report.getPaperFormat().getWidth();
118: if (wantsMoreData) {
119: doStart();
120: startPage();
121: }
122: }
123:
124: /**
125: * Called by <code>start</code> as a chance to insert behavior when the
126: * report starts.
127: */
128: protected void doStart() {
129: }
130:
131: /**
132: * Called by the report at the end of a report run.
133: */
134: public void end() {
135: if (wantsMoreData) {
136: for (Iterator iter = report.footers().iterator(); iter
137: .hasNext();)
138: outputSection((Section) iter.next(), SECT_REPORT_FOOTER);
139: endPage(true);
140: doEnd();
141: }
142: closeOutput();
143: }
144:
145: /**
146: * Called by <code>end</code> as a chance to insert behavior when the
147: * report ends.
148: */
149: protected void doEnd() {
150: }
151:
152: /**
153: * Called by <code>end</code> to let this layout engine clean up a bit.
154: */
155: protected void closeOutput() {
156: if (out != null) {
157: out.flush();
158: out.close();
159: out = null;
160: }
161: }
162:
163: /**
164: * Called by the report when group headers need to be output. Once one
165: * group header is output, we output all remaining group headers.
166: * <p>
167: * We need to explicitly evaluate all formulas in the headers that will be
168: * output because {@link #checkRemainingPageLength} causes formulas in
169: * detail and footers to be evaluated. Those formulas may depend upon
170: * values in these headers.
171: *
172: * @param isLastRow if <code>true</code>, this is the last row of the report
173: */
174: public void groupHeaders(boolean isLastRow) {
175: if (!wantsMoreData)
176: return;
177:
178: boolean headerWasOutput = false;
179: for (Iterator iter = report.groups(); iter.hasNext();) {
180: Group g = (Group) iter.next();
181: if (headerWasOutput || g.isNewValue()) {
182: for (Iterator i2 = g.headers().iterator(); i2.hasNext();)
183: ((Section) i2.next()).evaluateFormulas();
184: headerWasOutput = true;
185: }
186: }
187: // boolean headerWasOutput;
188:
189: checkRemainingPageLength(isLastRow, true);
190:
191: headerWasOutput = false;
192: for (Iterator iter = report.groups(); iter.hasNext();) {
193: Group g = (Group) iter.next();
194: if (headerWasOutput || g.isNewValue()) {
195: for (Iterator i2 = g.headers().iterator(); i2.hasNext();)
196: outputSection((Section) i2.next(),
197: SECT_GROUP_HEADER);
198: headerWasOutput = true;
199: }
200: }
201: }
202:
203: /**
204: * Called by the report when a single detail row needs to be output.
205: *
206: * @param isLastRow if <code>true</code>, this is the last row of the report
207: */
208: public void detail(boolean isLastRow) {
209: if (!wantsMoreData)
210: return;
211:
212: checkRemainingPageLength(isLastRow, true);
213: for (Iterator iter = report.details().iterator(); iter
214: .hasNext();)
215: outputSection((Section) iter.next(), SECT_DETAIL);
216: }
217:
218: /**
219: * Called by the report when group footers need to be output. When one
220: * group footer is output, we make sure all of the footers before (above)
221: * it are also output.
222: *
223: * @param isLastRow if <code>true</code>, this is the last row of the report
224: */
225: public void groupFooters(boolean isLastRow) {
226: if (!wantsMoreData)
227: return;
228:
229: checkRemainingPageLength(isLastRow, false);
230:
231: // We need to output group footers backwards (Group n ... Group 1).
232: // When a group footer is output, make sure all of the footers before
233: // (above) it are also output. First, we walk the group list forwards
234: // so we can see which group changed first and grab all groups
235: // after that one.
236: boolean footerWasOutput = false;
237: ArrayList groupsToOutput = new ArrayList();
238: for (Iterator iter = report.groups(); iter.hasNext();) {
239: Group g = (Group) iter.next();
240: if (footerWasOutput || g.isNewValue() || isLastRow) {
241: if (footerWasOutput)
242: g.forceFooterOutput();
243: groupsToOutput.add(g);
244: footerWasOutput = true;
245: }
246: }
247:
248: // Now we reverse the list of groups and output them.
249: Collections.reverse(groupsToOutput);
250: for (Iterator iter = groupsToOutput.iterator(); iter.hasNext();) {
251: Group g = (Group) iter.next();
252: for (Iterator i2 = g.footers().iterator(); i2.hasNext();)
253: outputSection((Section) i2.next(), SECT_GROUP_FOOTER);
254: }
255: }
256:
257: /**
258: * Checks remaining page length and outputs a new page if we are at the
259: * bottom of the page.
260: *
261: * @param isLastRow if <code>true</code>, this is the last row of the report
262: * @param includeDetail if <code>true</code>, include height of detail
263: * sections
264: */
265: protected void checkRemainingPageLength(boolean isLastRow,
266: boolean includeDetail) {
267: // NOTE: need to do this dynamically each row because each one
268: // of the multiple detail sections may or may not be active for
269: // this row.
270: double detailHeight = includeDetail ? calcDetailHeight() : 0;
271: double footerHeight = calcPageFooterHeight();
272: if (isLastRow)
273: footerHeight += calcReportFooterHeight();
274:
275: if ((pageHeightUsed + footerHeight + detailHeight) > pageHeight())
276: endPage(isLastRow);
277:
278: if (newPage)
279: startPage();
280: }
281:
282: /**
283: * Returns the current page number.
284: *
285: * @return the current page number
286: */
287: public int pageNumber() {
288: return pageNumber;
289: }
290:
291: /**
292: * Starts a new page.
293: */
294: protected void startPage() {
295: if (!wantsMoreData)
296: return;
297:
298: pageNumber += 1;
299: pageHeightUsed = 0;
300: newPage = false;
301:
302: doStartPage();
303: if (pageNumber == 1) {
304: for (Iterator iter = report.headers().iterator(); iter
305: .hasNext();)
306: outputSection((Section) iter.next(), SECT_REPORT_HEADER);
307: }
308: for (Iterator iter = report.pageHeaders().iterator(); iter
309: .hasNext();)
310: outputSection((Section) iter.next(), SECT_PAGE_HEADER);
311: }
312:
313: /**
314: * Called by <code>startPage</code> as a chance to insert behavior when a
315: * new page starts.
316: */
317: protected void doStartPage() {
318: }
319:
320: /**
321: * Ends a new page.
322: *
323: * @param isLastPage if <code>true</code>, this is the last page of the report
324: */
325: protected void endPage(boolean isLastPage) {
326: if (!wantsMoreData)
327: return;
328:
329: for (Iterator iter = report.pageFooters().iterator(); iter
330: .hasNext();)
331: outputSection((Section) iter.next(), SECT_PAGE_FOOTER);
332:
333: newPage = true;
334: doEndPage();
335: }
336:
337: /**
338: * Called by <code>endPage</code> as a chance to insert behavior when a
339: * new page ends.
340: */
341: protected void doEndPage() {
342: }
343:
344: /**
345: * Outputs a section.
346: *
347: * @param sect the section to output
348: * @param which the type of section (for example,
349: * <code>SECT_PAGE_FOOTER</code>)
350: */
351: protected void outputSection(Section sect, int which) {
352: if (!wantsMoreData)
353: return;
354:
355: if (sect.isVisibleForCurrentRow()) {
356: // Insert a page break if requested.
357: if (sect.hasPageBreak()
358: && previousSectionArea != SECT_PAGE_HEADER) {
359: endPage(false);
360: startPage();
361: }
362:
363: // This must be after page break because calling endPage() and
364: // startPage() changes the currentSection.
365: currentSection = sect;
366: // Insert a page break if section will get overwritten by the page footer.
367: if (sect.getArea().getArea() != SECT_PAGE_FOOTER
368: && pageHeight - pageHeightUsed
369: - calcPageFooterHeight() < currentSection
370: .getOutputHeight()) {
371: endPage(false);
372: startPage();
373: currentSection = sect;
374: }
375: report.evaluateFormulasIn(currentSection);
376: doOutputSection(currentSection);
377:
378: pageHeightUsed += sect.getOutputHeight();
379: previousSectionArea = which;
380: } else {
381: // Always eval formulas, even if the section is hidden
382: report.evaluateFormulasIn(sect);
383: }
384: }
385:
386: /**
387: * Called by <code>outputSection</code> as a chance to insert behavior
388: * when a section is output.
389: *
390: * @param sect a section
391: */
392: protected void doOutputSection(Section sect) {
393: // Output the fields in the section
394: for (Iterator iter = sect.fields(); iter.hasNext();) {
395: Field f = (Field) iter.next();
396: if (f.isVisible()) {
397: if (f instanceof ImageField)
398: outputImage((ImageField) f);
399: else
400: outputField(f);
401: }
402: }
403: // Output the lines
404: for (Iterator iter = sect.lines(); iter.hasNext();) {
405: Line l = (Line) iter.next();
406: if (l.isVisible())
407: outputLine(l);
408: }
409: }
410:
411: /**
412: * Outputs a field.
413: *
414: * @param field the field to output
415: */
416: protected void outputField(Field field) {
417: if (wantsMoreData) // Do nothing if we have cancelled
418: doOutputField(field);
419: }
420:
421: /**
422: * Called by <code>outputField</code> as a chance to insert behavior
423: * when a field is output.
424: *
425: * @param field a field
426: */
427: protected abstract void doOutputField(Field field);
428:
429: /**
430: * Outputs a image.
431: *
432: * @param image the image field to output
433: */
434: protected void outputImage(ImageField image) {
435: if (wantsMoreData) // Do nothing if we have cancelled
436: doOutputImage(image);
437: }
438:
439: /**
440: * Called by <code>outputImage</code> as a chance to insert behavior
441: * when a image is output.
442: *
443: * @param image an image field
444: */
445: protected abstract void doOutputImage(ImageField image);
446:
447: /**
448: * Outputs a line.
449: *
450: * @param line the line to output
451: */
452: protected void outputLine(Line line) {
453: if (wantsMoreData)
454: doOutputLine(line);
455: }
456:
457: /**
458: * Called by <code>outputLine</code> as a chance to insert behavior
459: * when a line is output.
460: *
461: * @param line a line
462: */
463: protected abstract void doOutputLine(Line line);
464:
465: /**
466: * Returns the current section type (header, footer, detail) as a string.
467: *
468: * @return a string representation of the current section type
469: */
470: protected String currentSectionTypeAsString() {
471: switch (currentSection.getArea().getArea()) {
472: case SectionArea.REPORT_HEADER:
473: return "report header";
474: case SectionArea.REPORT_FOOTER:
475: return "report footer";
476: case SectionArea.PAGE_HEADER:
477: return "page header";
478: case SectionArea.PAGE_FOOTER:
479: return "page footer";
480: case SectionArea.GROUP_HEADER:
481: return "group header";
482: case SectionArea.GROUP_FOOTER:
483: return "group footer";
484: case SectionArea.DETAIL:
485: return "detail";
486: default:
487: return "unknown"; // Should never happen
488: }
489: }
490:
491: /**
492: * Returns the total height of all sections in the specified list.
493: *
494: * @param area a section area
495: * @return the total height of all sections in the list
496: */
497: protected double calcSectionHeights(SectionArea area) {
498: double sum = 0;
499: for (Iterator iter = area.iterator(); iter.hasNext();) {
500: Section s = (Section) iter.next();
501: if (s.isVisibleForCurrentRow())
502: sum += s.getOutputHeight();
503: }
504: return sum;
505: }
506:
507: /**
508: * Returns the total height of all detail sections.
509: *
510: * @return the total height of all detail sections
511: */
512: protected double calcDetailHeight() {
513: return calcSectionHeights(report.details());
514: }
515:
516: /**
517: * Returns the total height of all page footer sections.
518: *
519: * @return the total height of all page footer sections
520: */
521: protected double calcPageFooterHeight() {
522: return calcSectionHeights(report.pageFooters());
523: }
524:
525: /**
526: * Returns the total height of all report footer sections.
527: *
528: * @return the total height of all report footer sections
529: */
530: protected double calcReportFooterHeight() {
531: return calcSectionHeights(report.footers());
532: }
533:
534: }
|