org.tellervo.desktop.print.SeriesReport.java Source code

Java tutorial

Introduction

Here is the source code for org.tellervo.desktop.print.SeriesReport.java

Source

/*******************************************************************************
 * Copyright (C) 2011 Peter Brewer.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Peter Brewer
 ******************************************************************************/
package org.tellervo.desktop.print;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

import org.apache.commons.lang.WordUtils;
import org.tellervo.desktop.core.App;
import org.tellervo.desktop.editor.DecadalModel;
import org.tellervo.desktop.editor.UnitAwareDecadalModel;
import org.tellervo.desktop.editor.WJTableModel;
import org.tellervo.desktop.io.Metadata;
import org.tellervo.desktop.prefs.Prefs.PrefKey;
import org.tellervo.desktop.remarks.Remarks;
import org.tellervo.desktop.sample.Sample;
import org.tellervo.desktop.ui.Alert;
import org.tellervo.desktop.ui.Builder;
import org.tellervo.desktop.util.labels.LabBarcode;
import org.tellervo.desktop.util.pdf.PrintablePDF;
import org.tridas.interfaces.ITridasSeries;
import org.tridas.schema.ComplexPresenceAbsence;
import org.tridas.schema.NormalTridasRemark;
import org.tridas.schema.NormalTridasUnit;
import org.tridas.schema.PresenceAbsence;
import org.tridas.schema.TridasDerivedSeries;
import org.tridas.schema.TridasElement;
import org.tridas.schema.TridasLastRingUnderBark;
import org.tridas.schema.TridasMeasurementSeries;
import org.tridas.schema.TridasObject;
import org.tridas.schema.TridasRadius;
import org.tridas.schema.TridasRemark;
import org.tridas.schema.TridasSample;
import org.tridas.schema.TridasWoodCompleteness;
import org.tridas.schema.Year;

import com.lowagie.text.Chunk;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.ColumnText;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;

public class SeriesReport extends ReportBase {

    /** A map for the lazy-loading of icons */
    private HashMap<String, Image> lazyIconMap;

    NormalTridasUnit displayUnits;

    private Sample s = new Sample();

    public SeriesReport(Sample s) {
        this.s = s;

        // lazily load icons
        lazyIconMap = new HashMap<String, Image>();
    }

    private void generateSeriesReport(OutputStream output) {

        displayUnits = NormalTridasUnit
                .valueOf(App.prefs.getPref(PrefKey.DISPLAY_UNITS, NormalTridasUnit.MICROMETRES.name().toString()));

        try {

            PdfWriter writer = PdfWriter.getInstance(document, output);
            document.setPageSize(PageSize.LETTER);
            document.open();
            cb = writer.getDirectContent();

            // Set basic metadata
            document.addAuthor("Peter Brewer");
            document.addSubject("Tellervo Series Report for " + s.getDisplayTitle());

            // Title Left      
            ColumnText ct = new ColumnText(cb);
            ct.setSimpleColumn(document.left(), document.top() - 163, 283, document.top(), 20, Element.ALIGN_LEFT);
            ct.addText(getTitlePDF());
            ct.go();

            // Barcode
            ColumnText ct2 = new ColumnText(cb);
            ct2.setSimpleColumn(370, document.top(15) - 100, document.right(0), document.top(0), 20,
                    Element.ALIGN_RIGHT);
            ct2.addElement(getBarCode());
            ct2.go();

            // Timestamp
            ColumnText ct3 = new ColumnText(cb);
            ct3.setSimpleColumn(document.left(), document.top() - 223, 283, document.top() - 60, 20,
                    Element.ALIGN_LEFT);
            ct3.setLeading(0, 1.2f);
            ct3.addText(getTimestampPDF());
            ct3.go();

            // Authorship
            ColumnText ct4 = new ColumnText(cb);
            ct4.setSimpleColumn(284, document.top() - 223, document.right(10), document.top() - 60, 20,
                    Element.ALIGN_RIGHT);
            ct4.setLeading(0, 1.2f);
            ct4.addText(getAuthorshipPDF());
            ct4.go();

            // Pad text
            document.add(new Paragraph(" "));
            Paragraph p2 = new Paragraph();
            p2.setSpacingBefore(50);
            p2.setSpacingAfter(10);
            p2.add(new Chunk(" ", bodyFont));
            document.add(new Paragraph(p2));

            // Ring width table
            getRingWidthTable();
            document.add(getParagraphSpace());

            if (s.getSeries() instanceof TridasMeasurementSeries) {
                // MEASUREMENT SERIES

                //document.add(getRingRemarks());
                document.add(getWoodCompletenessPDF());
                document.add(getParagraphSpace());
                document.add(getSeriesComments());
                document.add(getParagraphSpace());
                document.add(getInterpretationPDF());
                document.add(getParagraphSpace());
                document.add(getElementAndSampleInfo());
            } else {
                // DERIVED SERIES
                getWJTable();
                document.add(getParagraphSpace());
                document.add(getSeriesComments());
                document.add(getParagraphSpace());
                //document.add(getRingRemarks());

            }

        } catch (DocumentException de) {
            System.err.println(de.getMessage());
        }

        // Close the document
        document.close();
    }

    /**
     * Get an iText Paragraph for the Title 
     * 
     * @return Paragraph
     */
    private Paragraph getTitlePDF() {
        Paragraph p = new Paragraph();

        p.add(new Chunk(s.getDisplayTitle() + "\n", titleFont));

        // Add object name if this is a mSeries
        if (s.getSeries() instanceof TridasMeasurementSeries) {
            TridasObject tobj = s.getMeta(Metadata.OBJECT, TridasObject.class);

            p.add(new Chunk(tobj.getTitle(), subTitleFont));
        }
        return p;
    }

    /**
     * Create a series bar code for this series
     * 
     * @return Image 
     */
    private Image getBarCode() {
        UUID uuid = UUID.fromString(s.getSeries().getIdentifier().getValue());
        LabBarcode barcode = new LabBarcode(LabBarcode.Type.SERIES, uuid);

        //barcode.setX(0.4f);
        //barcode.setSize(1f);
        //barcode.setBarHeight(10f);
        barcode.setBaseline(-1f);
        //barcode.setFont(null);

        Image image = barcode.createImageWithBarcode(cb, null, null);

        image.setWidthPercentage(95);

        return image;

    }

    /**
     * Get PdfPTable containing the ring width data for this series
     * 
     * @return PdfPTable
     * @throws DocumentException 
     */
    private void getRingWidthTable() throws DocumentException {

        try {
            getDataTable(false);
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Get PdfPTable containing weiserjahre data for this series
     * 
     * @return
     * @throws DocumentException 
     */
    private void getWJTable() throws DocumentException {
        try {
            getDataTable(true);
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void getTableKey() throws DocumentException, IOException, IOException {
        PdfPTable mainTable = new PdfPTable(12);
        float[] widths = { 0.1f, 0.1f, 0.8f, 0.1f, 0.1f, 0.8f, 0.1f, 0.1f, 0.8f, 0.1f, 0.1f, 0.8f };
        mainTable.setWidths(widths);
        mainTable.setWidthPercentage(100);

        PdfPTable userRemarksTable = new PdfPTable(2);
        float[] widths2 = { 0.083f, 0.92f };
        userRemarksTable.setWidths(widths2);
        userRemarksTable.setWidthPercentage(100);

        Boolean userRemarkUsed = false;

        DecadalModel model;
        model = new UnitAwareDecadalModel(s);
        int rows = model.getRowCount();
        List<TridasRemark> masterList = null;

        // Loop through rows
        for (int row = 0; row < rows; row++) {
            // Loop through columns
            for (int col = 0; col < 11; col++) {
                org.tellervo.desktop.Year year = model.getYear(row, col);
                List<TridasRemark> remarksList = null;
                remarksList = s.getRemarksForYear(year);

                // If masterlist is still null initialize it with this remarks list
                if (remarksList.size() > 0 && masterList == null)
                    masterList = remarksList;

                for (TridasRemark remark : remarksList) {
                    if (!masterList.contains(remark))
                        masterList.add(remark);
                }
            }
        }

        for (TridasRemark remark : masterList) {
            PdfPCell iconCell = new PdfPCell();
            PdfPCell equalsCell = new PdfPCell();
            PdfPCell descriptionCell = new PdfPCell();

            iconCell.setBorder(0);
            equalsCell.setBorder(0);
            descriptionCell.setBorder(0);

            iconCell.setVerticalAlignment(Element.ALIGN_TOP);
            equalsCell.setVerticalAlignment(Element.ALIGN_TOP);
            descriptionCell.setVerticalAlignment(Element.ALIGN_TOP);

            Image icon = null;
            String remarkStr = null;

            // Get actual icon (either tridas or tellervo)
            if (remark.isSetNormalTridas()) {
                remarkStr = remark.getNormalTridas().toString().toLowerCase();
                remarkStr = remarkStr.replace("_", " ");
                icon = getTridasIcon(remark.getNormalTridas());
                if (icon == null)
                    icon = Builder.getITextImageMissingIcon();
            } else if (TELLERVO.equals(remark.getNormalStd())) {
                remarkStr = remark.getNormal();
                icon = getCorinaIcon(remark.getNormal());
                if (icon == null)
                    icon = Builder.getITextImageMissingIcon();
            } else {
                if (!userRemarkUsed) {
                    remarkStr = "User Remark (See Below)";
                    icon = Builder.getITextImageIcon("user.png");
                    userRemarkUsed = true;
                } else {
                    // User remark and we already have a key for this so continue
                    continue;
                }
            }

            iconCell.addElement(icon);
            equalsCell.addElement(new Phrase("=", tableBodyFont));
            descriptionCell.addElement(new Phrase(WordUtils.capitalize(remarkStr), tableBodyFont));

            mainTable.addCell(iconCell);
            mainTable.addCell(equalsCell);
            mainTable.addCell(descriptionCell);
        }

        // Pad out empty cells
        PdfPCell blankCell = new PdfPCell();
        blankCell.addElement(new Phrase(""));
        blankCell.setBorder(0);
        for (int i = 0; i < 12; i++) {
            mainTable.addCell(blankCell);
        }

        document.add(mainTable);

        if (userRemarkUsed) {
            PdfPCell yearCell = new PdfPCell();
            yearCell.setBorder(0);
            yearCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
            yearCell.addElement(new Phrase("Year", tableHeaderFont));
            userRemarksTable.addCell(yearCell);

            PdfPCell remarkCell = new PdfPCell();
            remarkCell.setBorder(0);
            remarkCell.addElement(new Phrase("User ring remarks", tableHeaderFont));
            userRemarksTable.addCell(remarkCell);

            for (int row = 0; row < rows; row++) {
                // Loop through columns
                for (int col = 0; col < 11; col++) {
                    org.tellervo.desktop.Year year = model.getYear(row, col);
                    List<TridasRemark> remarksList = null;
                    remarksList = s.getRemarksForYear(year);

                    for (TridasRemark remark : remarksList) {
                        if (remark.isSetNormalTridas() || remark.isSetNormalStd())
                            continue;

                        yearCell = new PdfPCell();
                        yearCell.setBorder(0);
                        yearCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
                        yearCell.addElement(new Phrase(year.toString(), tableHeaderFont));
                        userRemarksTable.addCell(yearCell);

                        remarkCell = new PdfPCell();
                        remarkCell.setBorder(0);
                        remarkCell.addElement(new Phrase(remark.getValue(), tableBodyFont));
                        userRemarksTable.addCell(remarkCell);
                    }
                }
            }

            document.add(userRemarksTable);
        }
    }

    /**
     * Get PdfPTable containing the ring width data for this series
     * 
     * @return PdfPTable
     * @throws DocumentException 
     * @throws IOException 
     * @throws MalformedURLException 
     */
    private void getDataTable(Boolean wj) throws DocumentException, MalformedURLException, IOException {
        // THE actual table
        PdfPTable mainTable = new PdfPTable(11);
        // Cell for column headers
        PdfPCell colHeadCell = new PdfPCell();
        // Model for data
        DecadalModel model;
        // Flag to show if there are *any* ring remarks
        Boolean hasRemarks = false;

        float[] columnWidths = new float[] { 20f, 8f, 8f, 8f, 8f, 8f, 8f, 8f, 8f, 8f, 8f };
        mainTable.setWidths(columnWidths);
        mainTable.setWidthPercentage(100f);

        if (wj == true) {
            if (s.hasWeiserjahre() == true) {
                model = new WJTableModel(s);
                document.add(new Chunk("Weiserjahre:", subSubSectionFont));
            } else {
                return;
            }
        } else {
            model = new UnitAwareDecadalModel(s);
            document.add(new Chunk("Ring widths:", subSubSectionFont));
        }

        int rows = model.getRowCount();

        // Do column headers
        if (wj == true) {
            colHeadCell.setPhrase(new Phrase("inc/dec", tableHeaderFont));
        } else if (this.s.getTridasUnits() == null) {
            // Unitless
            colHeadCell.setPhrase(new Phrase(" ", tableHeaderFont));
        } else {
            // Normal tridas units
            try {
                /*if(this.s.getTridasUnits().getNormalTridas().equals(NormalTridasUnit.MICROMETRES))
                {
                   colHeadCell.setPhrase(new Phrase("microns", tableHeaderFont));
                }*/

                // Use the current default display units

                colHeadCell.setPhrase(new Phrase(displayUnits.value(), tableHeaderFont));

                /*if(displayUnits.equals(NormalTridasUnit.MICROMETRES))
                {
                   colHeadCell.setPhrase(new Phrase("microns", tableHeaderFont));
                }
                else if(displayUnits.equals(NormalTridasUnit.HUNDREDTH_MM))
                {
                   colHeadCell.setPhrase(new Phrase("1/100th mm", tableHeaderFont));
                }
                */

            } catch (Exception e) {
                colHeadCell.setPhrase(new Phrase(" ", tableHeaderFont));
            }
        }
        colHeadCell.setBorderWidthBottom(headerLineWidth);
        colHeadCell.setBorderWidthTop(headerLineWidth);
        colHeadCell.setBorderWidthLeft(headerLineWidth);
        colHeadCell.setBorderWidthRight(headerLineWidth);
        mainTable.addCell(colHeadCell);
        for (int i = 0; i < 10; i++) {
            colHeadCell.setPhrase(new Phrase(Integer.toString(i), tableHeaderFont));
            colHeadCell.setHorizontalAlignment(Element.ALIGN_CENTER);
            colHeadCell.setBorderWidthBottom(headerLineWidth);
            colHeadCell.setBorderWidthTop(headerLineWidth);
            colHeadCell.setBorderWidthLeft(lineWidth);
            colHeadCell.setBorderWidthRight(lineWidth);

            if (i == 0)
                colHeadCell.setBorderWidthLeft(headerLineWidth);
            if (i == 9)
                colHeadCell.setBorderWidthRight(headerLineWidth);
            mainTable.addCell(colHeadCell);
        }

        // Loop through rows
        for (int row = 0; row < rows; row++) {
            // Loop through columns
            for (int col = 0; col < 11; col++) {
                // Mini table to hold remark icons
                PdfPTable remarksMiniTable = new PdfPTable(3);
                float[] widths = { 0.3f, 0.3f, 0.6f };
                remarksMiniTable.setWidths(widths);
                remarksMiniTable.setWidthPercentage(100);

                // Get ring value or year number for first column
                Phrase cellValuePhrase = null;
                Object value = model.getValueAt(row, col);
                if (value == null) {
                    cellValuePhrase = new Phrase("");
                } else {
                    /*if(displayUnits.equals(NormalTridasUnit.HUNDREDTH_MM))
                    {
                       try{
                       Integer val = (Integer) value;
                       val =val/10;
                       cellValuePhrase = new Phrase(String.valueOf(val), getTableFont(col));
                       } catch (Exception e){
                          cellValuePhrase = new Phrase(value.toString(), getTableFont(col));
                        
                       }
                    }
                    else if(displayUnits.equals(NormalTridasUnit.FIFTIETH_MM))
                    {
                       try{
                       Integer val = (Integer) value;
                       val =val/20;
                       cellValuePhrase = new Phrase(String.valueOf(val), getTableFont(col));
                       } catch (Exception e){
                          cellValuePhrase = new Phrase(value.toString(), getTableFont(col));
                        
                       }               
                    }
                    else if(displayUnits.equals(NormalTridasUnit.TWENTIETH_MM))
                    {
                       try{
                       Integer val = (Integer) value;
                       val =val/50;
                       cellValuePhrase = new Phrase(String.valueOf(val), getTableFont(col));
                       } catch (Exception e){
                          cellValuePhrase = new Phrase(value.toString(), getTableFont(col));
                        
                       }               
                    }
                    else if(displayUnits.equals(NormalTridasUnit.TENTH_MM))
                    {
                       try{
                       Integer val = (Integer) value;
                       val =val/100;
                       cellValuePhrase = new Phrase(String.valueOf(val), getTableFont(col));
                       } catch (Exception e){
                          cellValuePhrase = new Phrase(value.toString(), getTableFont(col));
                        
                       }               
                    }
                    else if(displayUnits.equals(NormalTridasUnit.MICROMETRES))
                    {*/
                    cellValuePhrase = new Phrase(value.toString(), getTableFont(col));
                    //}
                }

                // Get any remarks and compile them into a mini table
                org.tellervo.desktop.Year year = model.getYear(row, col);
                List<TridasRemark> remarksList = null;
                remarksList = s.getRemarksForYear(year);

                // If there are remarks, cycle through them adding cells to the mini table
                if (col != 0 && remarksList.size() > 0) {
                    hasRemarks = true;
                    // Get icons for remarks
                    int cellnum = 1;
                    int remarknum = 0;
                    for (TridasRemark remark : remarksList) {
                        // Keep track of which remark we are on.
                        remarknum++;
                        // String for holding remark name for debugging
                        String remstr = "?";
                        // The actual remark icon
                        Image icon = null;
                        // A table cell for the remark
                        PdfPCell remarkCell = new PdfPCell();

                        // Set default attributes for remark and value cells
                        remarkCell.setBorderWidthBottom(0);
                        remarkCell.setBorderWidthTop(0);
                        remarkCell.setBorderWidthLeft(0);
                        remarkCell.setBorderWidthRight(0);
                        remarkCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
                        remarkCell.setPadding(0);
                        remarkCell.setUseBorderPadding(true);

                        // A table cell for the ring width value
                        PdfPCell valueCell = new PdfPCell();
                        valueCell = remarkCell;

                        // Get actual icon (either tridas or tellervo)
                        if (remark.isSetNormalTridas()) {
                            remstr = remark.getNormalTridas().toString();
                            icon = getTridasIcon(remark.getNormalTridas());
                            if (icon == null)
                                icon = Builder.getITextImageMissingIcon();
                        } else if (TELLERVO.equals(remark.getNormalStd())) {
                            remstr = remark.getNormal();
                            icon = getCorinaIcon(remark.getNormal());
                            if (icon == null)
                                icon = Builder.getITextImageMissingIcon();
                        } else {
                            if (remark.isSetValue()) {
                                remstr = remark.getValue();
                            } else if (remark.isSetNormal()) {
                                remstr = remark.getNormal();
                            } else {
                                remstr = "Unknown";
                            }
                            icon = Builder.getITextImageIcon("user.png");

                        }

                        // Print debug info for this remark
                        String errStr = "Getting icon for " + remstr + " for year " + year.toString()
                                + "(cell value = " + cellnum + ")";
                        System.out.print(errStr);

                        // Shrink the icon a bit
                        icon.scalePercent(20);

                        // Add icon to minitable
                        remarkCell.addElement(icon);
                        remarksMiniTable.addCell(remarkCell);
                        cellnum++;

                        if (cellnum == 1 && remarksList.size() < cellnum) {
                            // First cell and no remark so print blank
                            valueCell.setPhrase(new Phrase(""));
                            remarksMiniTable.addCell(valueCell);
                            cellnum++;
                        }
                        if (cellnum == 2 && remarksList.size() < cellnum) {
                            // Second cell and no remark so print blank
                            valueCell.setPhrase(new Phrase(""));
                            remarksMiniTable.addCell(valueCell);
                            cellnum++;
                        }
                        if (cellnum == 3) {
                            // In third cell so print value
                            valueCell.setPhrase(cellValuePhrase);
                            remarksMiniTable.addCell(valueCell);
                            cellnum++;
                        } else if (cellnum % 3 == 0) {
                            // In third column so print blank
                            valueCell.setPhrase(new Phrase(""));
                            remarksMiniTable.addCell(valueCell);
                            cellnum++;
                        }

                        if (remarknum == remarksList.size()) {
                            valueCell.setPhrase(new Phrase(""));
                            remarksMiniTable.addCell(valueCell);
                            remarksMiniTable.addCell(valueCell);
                        }

                        remarkCell = null;
                        valueCell = null;
                    }
                } else {
                    // No remarks so make mini table have blank, blank, value

                    // Create blank and value cells
                    PdfPCell blankCell = new PdfPCell();
                    PdfPCell valueCell = new PdfPCell();

                    // Set up style
                    blankCell.setBorderWidthBottom(0);
                    blankCell.setBorderWidthTop(0);
                    blankCell.setBorderWidthLeft(0);
                    blankCell.setBorderWidthRight(0);
                    blankCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
                    blankCell.setPadding(0);
                    blankCell.setUseBorderPadding(true);
                    valueCell = blankCell;

                    // Add cells to mini table
                    remarksMiniTable.addCell(blankCell);
                    remarksMiniTable.addCell(blankCell);
                    valueCell.setPhrase(cellValuePhrase);
                    remarksMiniTable.addCell(valueCell);
                }

                // Set border styles depending on where we are in the table

                // Defaults
                PdfPCell mainTableCell = new PdfPCell();
                mainTableCell.setBorderWidthBottom(lineWidth);
                mainTableCell.setBorderWidthTop(lineWidth);
                mainTableCell.setBorderWidthLeft(lineWidth);
                mainTableCell.setBorderWidthRight(lineWidth);
                mainTableCell.setHorizontalAlignment(Element.ALIGN_RIGHT);

                // Row headers
                if (col == 0) {
                    mainTableCell.setHorizontalAlignment(Element.ALIGN_LEFT);
                    mainTableCell.setBorderWidthLeft(headerLineWidth);
                    mainTableCell.setBorderWidthRight(headerLineWidth);
                }

                // First data column
                if (col == 1) {
                    mainTableCell.setBorderWidthLeft(headerLineWidth);
                }

                // Last data column
                if (col == 10) {
                    mainTableCell.setBorderWidthRight(headerLineWidth);
                }

                // Last row
                if (row == model.getRowCount() - 1) {
                    mainTableCell.setBorderWidthBottom(headerLineWidth);
                }

                // Write mini table to cell       
                mainTableCell.addElement(remarksMiniTable);

                //mainTableCell.addElement(userRemarksMiniTable);

                // Write cell to main table
                mainTable.addCell(mainTableCell);

            }
        }

        // Add table to document
        document.add(mainTable);

        if (!wj && hasRemarks)
            getTableKey();
    }

    /**
     * iText paragraph containing created and lastmodified timestamps
     * 
     * @return Paragraph
     */
    private Paragraph getTimestampPDF() {
        // Set up calendar
        Date createdTimestamp = s.getSeries().getCreatedTimestamp().getValue().toGregorianCalendar().getTime();
        Date lastModifiedTimestamp = s.getSeries().getLastModifiedTimestamp().getValue().toGregorianCalendar()
                .getTime();
        DateFormat df1 = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT);

        Paragraph p = new Paragraph();

        p.add(new Chunk("Created: ", subSubSectionFont));
        p.add(new Chunk(df1.format(createdTimestamp), bodyFont));
        p.add(new Chunk("\nLast Modified: ", subSubSectionFont));
        p.add(new Chunk(df1.format(lastModifiedTimestamp), bodyFont));

        return p;

    }

    /**
     * iText Paragraph containing the various authorship fields
     * @return Paragraph
     */
    private Paragraph getAuthorshipPDF() {
        Paragraph p = new Paragraph();

        ITridasSeries sss = s.getSeries();
        TridasMeasurementSeries mseries = null;
        TridasDerivedSeries dseries = null;

        if (sss instanceof TridasMeasurementSeries) {
            mseries = (TridasMeasurementSeries) sss;
            if (mseries.getAnalyst() != null) {
                p.add(new Chunk("Measured by: ", subSubSectionFont));
                p.add(new Chunk(mseries.getAnalyst(), bodyFont));
            }

            if (mseries.getDendrochronologist() != null) {
                p.add(new Chunk("\nSupervised by: ", subSubSectionFont));
                p.add(new Chunk(mseries.getDendrochronologist(), bodyFont));
            }
        } else {
            dseries = (TridasDerivedSeries) sss;
            p.add(new Chunk("Created by: ", subSubSectionFont));
            p.add(new Chunk(dseries.getAuthor(), bodyFont));

        }

        return p;

    }

    /**
     * Get the font style to use in the table based on column number
     * @param col column number
     * @return Font
     */
    private Font getTableFont(int col) {
        if (col == 0) {
            return tableHeaderFont;
        } else {
            return tableBodyFont;
        }

    }

    private Paragraph getSeriesComments() {

        Paragraph p = new Paragraph();

        if (s.getSeries().getComments() != null) {

            p.setLeading(0, 1.2f);
            p.add(new Chunk("Comments: \n", subSubSectionFont));
            p.add(new Chunk(s.getSeries().getComments(), bodyFont));
            return p;
        } else {
            return p;
        }

    }

    private Paragraph getInterpretationPDF() {

        Paragraph p = new Paragraph();
        p.setLeading(0, 1.2f);
        Year firstyear = s.getSeries().getInterpretation().getFirstYear();
        Year pithYear = s.getSeries().getInterpretation().getPithYear();
        Year deathyear = s.getSeries().getInterpretation().getDeathYear();
        Boolean isRelativelyDated = false;
        p.add(new Chunk("Interpretation:", subSubSectionFont));

        String datingType = s.getSeries().getInterpretation().getDating().getType().toString();

        if (datingType == "RELATIVE")
            isRelativelyDated = true;

        if (firstyear != null) {
            p.add(new Chunk("\n- The first ring of this series begins in ", bodyFont));
            if (isRelativelyDated)
                p.add(new Chunk("relative year ", bodyFont));

            if (firstyear.getCertainty() != null) {
                p.add(new Chunk(firstyear.getCertainty().toString().toLowerCase() + " ", bodyFont));
            }
            p.add(new Chunk(firstyear.getValue().toString(), bodyFont));
            if (isRelativelyDated == false)
                p.add(new Chunk(firstyear.getSuffix().toString(), bodyFont));
            p.add(new Chunk(".\n", bodyFont));
        }

        if (pithYear != null && deathyear != null) {
            p.add(new Chunk("- The pith of this radius was laid down ", bodyFont));
            if (pithYear.getCertainty() != null) {
                p.add(certaintyToNaturalString(pithYear.getCertainty().toString()));
            }
            if (isRelativelyDated)
                p.add(new Chunk("relative year ", bodyFont));
            p.add(new Chunk(pithYear.getValue().toString(), bodyFont));
            if (isRelativelyDated == false)
                p.add(new Chunk(pithYear.getSuffix().toString(), bodyFont));
            p.add(new Chunk(" and died ", bodyFont));
            if (deathyear.getCertainty() != null) {
                p.add(certaintyToNaturalString(deathyear.getCertainty().toString()));
            }
            if (isRelativelyDated)
                p.add(new Chunk("relative year ", bodyFont));
            p.add(new Chunk(deathyear.getValue().toString(), bodyFont));
            if (isRelativelyDated == false)
                p.add(new Chunk(deathyear.getSuffix().toString(), bodyFont));
            p.add(new Chunk(".\n", bodyFont));

        }

        // Dated with...
        if (s.getSeries().getInterpretation().getDatingReference() != null) {
            p.add(new Chunk("\n- This series was dated using series: " + s.getSeries().getInterpretation()
                    .getDatingReference().getLinkSeries().getIdentifier().getValue().toString(), bodyFont));
        }

        // Provence...
        if (s.getSeries().getInterpretation().getProvenance() != null) {
            p.add(new Chunk("\n- Provenance: " + s.getSeries().getInterpretation().getProvenance().toString(),
                    bodyFont));
        }

        return p;
    }

    private Chunk certaintyToNaturalString(String certainty) {
        Chunk c = new Chunk();

        if (certainty.equalsIgnoreCase("exact")) {
            c.append("in exactly ");
        } else if (certainty.equalsIgnoreCase("after")) {
            c.append("after ");
        } else if (certainty.equalsIgnoreCase("before")) {
            c.append("before ");
        } else {
            c.append("in " + certainty.toLowerCase() + " ");
        }

        c.setFont(bodyFont);
        return c;
    }

    /**
     * iText paragraph for the TRiDaS wood completeness fields
     * 
     * @return Paragraph
     */
    private Paragraph getWoodCompletenessPDF() {
        Paragraph p = new Paragraph();
        p.setLeading(0, 1.2f);
        TridasRadius trad = s.getMeta(Metadata.RADIUS, TridasRadius.class);
        TridasWoodCompleteness woodCompleteness = trad.getWoodCompleteness();

        String pithPresence = null;
        String barkPresence = null;

        p.add(new Chunk("Wood Completeness:\n", subSubSectionFont));

        // Extract pith info
        if (woodCompleteness.getPith() != null) {

            if (woodCompleteness.getPith().getPresence() != null) {
                pithPresence = woodCompleteness.getPith().getPresence().value();
            } else {
                pithPresence = "not specified";
            }

            p.add(new Chunk("- Pith is " + pithPresence + ".\n", bodyFont));
        } else {
            p.add(new Chunk("- No pith details were recorded.\n", bodyFont));
        }

        // Ring count 
        p.add(new Chunk("- A total of " + String.valueOf(s.countRings()) + " rings were measured.", bodyFont));

        // Unmeasured rings
        if (woodCompleteness.isSetNrOfUnmeasuredInnerRings() || woodCompleteness.isSetNrOfUnmeasuredOuterRings()) {
            String txt = " Additional rings were observed but not measured (";
            if (woodCompleteness.isSetNrOfUnmeasuredInnerRings()) {
                txt += String.valueOf(woodCompleteness.getNrOfUnmeasuredInnerRings()) + " towards the pith";
                if (woodCompleteness.isSetNrOfUnmeasuredOuterRings()) {
                    txt += " and ";
                }
            }
            if (woodCompleteness.isSetNrOfUnmeasuredOuterRings()) {
                txt += String.valueOf(woodCompleteness.getNrOfUnmeasuredOuterRings()) + " towards the bark";
            }

            txt += ").";

            p.add(new Chunk(txt, bodyFont));
            p.add(new Chunk("\n"));
        } else {
            p.add(new Chunk("\n"));
        }

        // Extract Heartwood and sapwood info
        p.add(getHeartSapwoodDetails(woodCompleteness, WoodType.HEARTWOOD));
        p.add(getHeartSapwoodDetails(woodCompleteness, WoodType.SAPWOOD));

        // Extract last ring under bark info
        if (woodCompleteness.getSapwood().getLastRingUnderBark() != null) {
            TridasLastRingUnderBark lastRing = woodCompleteness.getSapwood().getLastRingUnderBark();
            if (lastRing.getPresence() != null) {
                if (lastRing.getPresence().equals(PresenceAbsence.PRESENT)) {
                    p.add(new Chunk("- Last ring under bark is present", bodyFont));

                    if (lastRing.getContent() != null) {
                        p.add(new Chunk(
                                " and the user has noted that it is: " + lastRing.getContent().toString() + ".\n",
                                bodyFont));
                    } else {
                        p.add(new Chunk(".\n", bodyFont));
                    }

                } else if (lastRing.getPresence().equals(PresenceAbsence.ABSENT)) {
                    p.add(new Chunk("- Last ring under bark is absent.\n", bodyFont));
                }
            }

        }

        // Extract bark info
        if (woodCompleteness.getBark() != null) {
            if (woodCompleteness.getBark().getPresence().toString().toLowerCase() != null) {
                if (woodCompleteness.getBark().getPresence().value() == "present") {
                    p.add(new Chunk("- Bark is present ", bodyFont));
                    // Last ring info
                    if (woodCompleteness.getSapwood().getLastRingUnderBark() != null) {
                        p.add(new Chunk("and the last ring before the bark is noted as: \""
                                + woodCompleteness.getSapwood().getLastRingUnderBark().getContent().toString()
                                + "\"", bodyFont));
                    } else {
                        p.add(new Chunk("but no details about the last ring under the bark were recorded.\n",
                                bodyFont));
                    }
                } else if (woodCompleteness.getBark().getPresence().value() == "absent") {

                    // Calculate if we have waney edge
                    if (woodCompleteness.getSapwood().getPresence().equals(ComplexPresenceAbsence.COMPLETE)) {
                        p.add(new Chunk("- Waney edge present\n", bodyFont));
                    } else {
                        p.add(new Chunk("- Bark is absent.\n", bodyFont));
                    }
                } else {
                    barkPresence = woodCompleteness.getBark().getPresence().value();
                    p.add(new Chunk("- Bark is " + barkPresence + "\n", bodyFont));
                }
            } else {
                p.add(new Chunk("- Bark information was not recorded.\n", bodyFont));
            }
        }

        return p;
    }

    private Paragraph getHeartSapwoodDetails(TridasWoodCompleteness woodCompleteness, WoodType type) {
        Paragraph p = new Paragraph();

        String presence = null;
        String presenceStr = null;
        String missingRings = null;
        String missingRingsStr = null;
        String foundationStr = null;
        String woodTypeStr = null;
        Integer nrSapwoodRings = null;

        // Extract data from woodcompleteness based on type
        if (type == WoodType.HEARTWOOD) {
            // Extract Heartwood details
            woodTypeStr = "heartwood";
            if (woodCompleteness.getHeartwood() != null) {

                // Presence / Absence
                if (woodCompleteness.getHeartwood().getPresence() != null) {
                    if (woodCompleteness.getHeartwood().getPresence().equals(ComplexPresenceAbsence.UNKNOWN)) {
                        // if heartwood presence is unknown all other details are irrelevant
                        p.add(new Chunk("- No " + woodTypeStr + " details recorded.", bodyFont));
                        return p;
                    } else {
                        presence = woodCompleteness.getHeartwood().getPresence().value();
                    }
                }

                // Missing rings
                if (woodCompleteness.getHeartwood().getMissingHeartwoodRingsToPith() != null) {
                    missingRingsStr = woodCompleteness.getHeartwood().getMissingHeartwoodRingsToPith().toString();
                    missingRings = missingRingsStr;
                } else {
                    missingRingsStr = "an unknown number of";
                }

                // Missing rings foundation
                if (woodCompleteness.getHeartwood().getMissingHeartwoodRingsToPithFoundation() != null) {
                    foundationStr = woodCompleteness.getHeartwood().getMissingHeartwoodRingsToPithFoundation()
                            .toString().toLowerCase();
                } else {
                    foundationStr = "unspecified reasons";
                }

            } else {
                p.add(new Chunk("- No " + woodTypeStr + " details recorded.", bodyFont));
                return p;
            }
        } else if (type == WoodType.SAPWOOD) {
            // Extract Sapwood details
            woodTypeStr = "sapwood";
            if (woodCompleteness.getSapwood() != null) {

                // Presence / Absence
                if (woodCompleteness.getSapwood().getPresence() != null) {
                    if (woodCompleteness.getSapwood().getPresence().equals(ComplexPresenceAbsence.UNKNOWN)) {
                        // if sapwood presence is unknown all other details are irrelevant
                        p.add(new Chunk("- No " + woodTypeStr + " details recorded.", bodyFont));
                        return p;
                    } else {
                        presence = woodCompleteness.getSapwood().getPresence().value();
                    }
                }

                // Missing rings
                if (woodCompleteness.getSapwood().getMissingSapwoodRingsToBark() != null) {
                    missingRingsStr = woodCompleteness.getSapwood().getMissingSapwoodRingsToBark().toString();
                    missingRings = missingRingsStr;
                } else {
                    missingRingsStr = "an unknown number of";
                }

                // No. of rings present
                if (woodCompleteness.getSapwood().getNrOfSapwoodRings() != null) {
                    nrSapwoodRings = woodCompleteness.getSapwood().getNrOfSapwoodRings();
                }

                // Missing rings foundation
                if (woodCompleteness.getSapwood().getMissingSapwoodRingsToBarkFoundation() != null) {
                    foundationStr = woodCompleteness.getSapwood().getMissingSapwoodRingsToBarkFoundation()
                            .toString().toLowerCase();
                } else {
                    foundationStr = "unspecified reasons";
                }
            } else {
                p.add(new Chunk("- No " + woodTypeStr + " details recorded.", bodyFont));
                return p;
            }

        } else {
            return null;
        }

        // Set output strings for presence/absence
        if (presence == "unknown") {
            //presenceStr = "- Whether " + woodTypeStr + " is present or not is unknown";
            presenceStr = "";
        } else if (presence == null) {
            //presenceStr = "- Presence of " + woodTypeStr + " has not been specified";
            presenceStr = "";
        } else {
            presenceStr = "- " + woodTypeStr.substring(0, 1).toUpperCase() + woodTypeStr.substring(1) + " is "
                    + presence;
            if (woodTypeStr == "sapwood" && nrSapwoodRings != null) {
                presenceStr += ". A total of " + nrSapwoodRings + " sapwood rings were identified";
            }
        }

        // Compile paragraph   
        p.add(new Chunk(presenceStr, bodyFont));
        if (missingRings != null) {
            p.add(new Chunk(". The analyst records that " + missingRingsStr, bodyFont));
            if (missingRingsStr.equals("1")) {
                p.add(new Chunk(" ring is ", bodyFont));
            } else {
                p.add(new Chunk(" rings are ", bodyFont));
            }
            p.add(new Chunk("missing, the justification of which is noted as: \"" + foundationStr + "\".",
                    bodyFont));
        } else if (presence == "complete") {
            // Wood is complete so no mention required about missing rings
            p.add(new Chunk(". ", bodyFont));
        } else {
            //p.add(new Chunk(". Details about missing " + woodTypeStr + " rings was not recorded.", bodyFont));
            p.add(new Chunk("", bodyFont));
        }

        return p;

    }

    private enum WoodType {
        HEARTWOOD, SAPWOOD
    }

    /**
     * iText paragraph of element and sample info
     * @return Paragraph
     */
    private Paragraph getElementAndSampleInfo() {
        Paragraph p = new Paragraph();

        TridasElement telem = s.getMeta(Metadata.ELEMENT, TridasElement.class);
        TridasSample tsamp = s.getMeta(Metadata.SAMPLE, TridasSample.class);

        p.add(new Chunk("Element and sample details:\n", subSubSectionFont));

        p.add(new Chunk("- Taxon:  ", bodyFont));
        p.add(new Chunk(telem.getTaxon().getNormal() + "\n", bodyFontItalic));
        p.add(new Chunk("- Element type: " + telem.getType().getNormal() + "\n", bodyFont));
        p.add(new Chunk("- Sample type: " + tsamp.getType().getNormal() + "\n", bodyFont));
        return p;

    }

    /**
     * Lazily-load this icon
     * 
     * @param iconName
     * @return the icon, or null if iconName was null
     */
    private Image lazyLoadIcon(String iconName) {
        if (iconName == null)
            return null;

        Image icon = lazyIconMap.get(iconName);
        if (icon == null) {
            // lazy-load the icon
            icon = Builder.getITextImageIcon(iconName);

            lazyIconMap.put(iconName, icon);
        }

        return icon;
    }

    /**
     * Get an icon for this tridas remark
     * @param remark
     * @return the icon, lazily loaded, or null
     */
    private Image getTridasIcon(NormalTridasRemark remark) {
        return lazyLoadIcon(Remarks.getTridasRemarkIcons().get(remark));
    }

    /**
     * Get an icon for this Corina remark (text)
     * @param remark
     * @return the icon, lazily loaded, or null
     */
    private Image getCorinaIcon(String remark) {

        String iconName = Remarks.getTellervoRemarkIcons().get(remark);

        return lazyLoadIcon(iconName);
    }

    private final static String TELLERVO = "Tellervo";

    /**
     * Function for printing or viewing series report
     * @param printReport Boolean
     * @param vmid String
     */
    private static void getReport(Boolean printReport, final Sample samp) {
        // create the series report
        SeriesReport report = new SeriesReport(samp);

        if (printReport) {
            ByteArrayOutputStream output = new ByteArrayOutputStream();

            report.generateSeriesReport(output);

            try {
                PrintablePDF pdf = PrintablePDF.fromByteArray(output.toByteArray());

                // true means show printer dialog, false means just print using the default printer
                pdf.print(true);
            } catch (Exception e) {
                e.printStackTrace();
                Alert.error("Printing error",
                        "An error occured during printing.\n  See error log for further details.");
            }
        } else {
            // probably better to use a chooser dialog here...
            try {
                File outputFile = File.createTempFile("seriesreport", ".pdf");
                FileOutputStream output = new FileOutputStream(outputFile);

                report.generateSeriesReport(output);

                App.platform.openFile(outputFile);
            } catch (IOException ioe) {
                ioe.printStackTrace();
                Alert.error("Error",
                        "An error occurred while generating the series report.\n  See error log for further details.");
                return;
            }
        }
    }

    /**
     * Wrapper function to print report
     *  
     * @param vmid
     */
    public static void printReport(Sample s) {
        getReport(true, s);
    }

    /**
     * Wrapper function to view report
     * @param vmid
     */
    public static void viewReport(Sample s) {
        getReport(false, s);
    }

}