org.unitime.timetable.export.solver.ExportTimetablePDF.java Source code

Java tutorial

Introduction

Here is the source code for org.unitime.timetable.export.solver.ExportTimetablePDF.java

Source

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/
package org.unitime.timetable.export.solver;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.unitime.localization.impl.Localization;
import org.unitime.timetable.export.ExportHelper;
import org.unitime.timetable.gwt.command.server.GwtRpcImplementation;
import org.unitime.timetable.gwt.resources.GwtConstants;
import org.unitime.timetable.gwt.resources.GwtMessages;
import org.unitime.timetable.gwt.shared.FilterInterface;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridBackground;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridCell;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridFilterRequest;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridFilterResponse;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridModel;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridRequest;
import org.unitime.timetable.gwt.shared.TimetableGridInterface.TimetableGridResponse;
import org.unitime.timetable.util.Formats;
import org.unitime.timetable.util.Formats.Format;
import org.unitime.timetable.util.PdfEventHandler;
import org.unitime.timetable.util.PdfFont;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.ColumnText;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfWriter;

/**
 * @author Tomas Muller
 */
@Service("org.unitime.timetable.export.Exporter:timetable.pdf")
public class ExportTimetablePDF extends TableExporter {
    protected static final GwtConstants CONSTANTS = Localization.create(GwtConstants.class);
    protected static final GwtMessages MESSAGES = Localization.create(GwtMessages.class);
    protected static Format<Date> sDateFormatMeeting = Formats.getDateFormat(Formats.Pattern.TIMETABLE_GRID_DATE);
    protected static Format<Number> sUtilizationFormat = Formats
            .getConcurrentNumberFormat(Formats.Pattern.UTILIZATION);
    protected static int sHeaderWidth = 100;
    protected static int sLineHeight = 13;

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public String reference() {
        return "timetable.pdf";
    }

    @Override
    public void export(ExportHelper helper) throws IOException {
        GwtRpcImplementation<TimetableGridFilterRequest, TimetableGridFilterResponse> filterService = (GwtRpcImplementation<TimetableGridFilterRequest, TimetableGridFilterResponse>) applicationContext
                .getBean(TimetableGridFilterRequest.class.getName());
        TimetableGridFilterResponse filter = filterService.execute(new TimetableGridFilterRequest(),
                helper.getSessionContext());
        fillInFilter(filter, helper);

        GwtRpcImplementation<TimetableGridRequest, TimetableGridResponse> service = (GwtRpcImplementation<TimetableGridRequest, TimetableGridResponse>) applicationContext
                .getBean(TimetableGridRequest.class.getName());
        TimetableGridRequest request = new TimetableGridRequest();
        request.setFilter(filter);
        TimetableGridResponse response = service.execute(request, helper.getSessionContext());

        printTables(filter, response, helper);
    }

    protected void printTables(FilterInterface filter, TimetableGridResponse response, ExportHelper helper)
            throws IOException {
        helper.setup("application/pdf", reference(), true);
        try {
            Document document = new Document();
            PdfWriter writer = PdfWriter.getInstance(document, helper.getOutputStream());
            writer.setPageEvent(new PdfEventHandler());
            int index = 0;
            int margin = 50;
            int width = 1000;
            for (TimetableGridModel model : response.getModels())
                width = Math.max(width, pageWidth(filter, model, response.getWeekOffset()));
            int displayMode = Integer.valueOf(filter.getParameterValue("dispMode", "0"));
            int height = (displayMode == 2 ? (2 * margin + width) * 22 / 17 - 2 * margin
                    : (2 * margin + width) * 17 / 22 - 2 * margin);
            for (TimetableGridModel model : response.getModels())
                height = Math.max(height, pageHeight(filter, model, response.getWeekOffset(), true));
            document.setPageSize(new Rectangle(width + 2 * margin, height + 2 * margin));
            document.setMargins(margin, margin, margin, margin);
            document.open();
            int used = 0;
            if (Integer.valueOf(filter.getParameterValue("dispMode", "0")) == 0) {
                boolean hasDay[] = { true, true, true, true, true, false, false };
                String days = filter.getParameterValue("days");
                if (days != null && days.length() == 7 && days.indexOf('1') >= 0) {
                    for (int i = 0; i < 7; i++)
                        hasDay[i] = (days.charAt(i) == '1');
                }
                for (int i = 0; i < 7; i++) {
                    if (!hasDay[i])
                        continue;
                    String d = "";
                    for (int j = 0; j < 7; j++)
                        d += (i == j ? "1" : "0");
                    filter.getParameter("days").setValue(d);
                    boolean first = true;
                    for (TimetableGridModel model : response.getModels()) {
                        if (used > 0
                                && pageHeight(filter, model, response.getWeekOffset(), first) > height - used) {
                            document.newPage();
                            used = 0;
                        }
                        TimetableGrid tg = new TimetableGrid(filter, model, index++, width,
                                response.getWeekOffset(), used == 0 || first);
                        PdfContentByte canvas = writer.getDirectContent();
                        tg.print(canvas, margin, margin + used, height + 2 * margin, null);
                        used += tg.getHeight();
                        first = false;
                    }
                    used += 25;
                }
            } else {
                for (TimetableGridModel model : response.getModels()) {
                    if (used > 0 && pageHeight(filter, model, response.getWeekOffset(), false) > height - used) {
                        document.newPage();
                        used = 0;
                    }
                    TimetableGrid tg = new TimetableGrid(filter, model, index++, width, response.getWeekOffset(),
                            used == 0);
                    PdfContentByte canvas = writer.getDirectContent();
                    tg.print(canvas, margin, margin + used, height + 2 * margin, null);
                    used += tg.getHeight() + 25;
                }
            }
            if (document != null)
                document.close();
        } catch (DocumentException e) {
            throw new IOException(e.getMessage(), e);
        }
    }

    protected static class P {
        private Integer iLeft, iTop, iRight, iBottom, iWidth, iHeight;
        private String iText, iStyle;
        private boolean iItalics = false;
        private String iColor, iBgColor;
        private List<P> iContent = new ArrayList<P>();

        public P(String style) {
            iStyle = style;
        }

        public void setWidth(int width) {
            iWidth = width;
        }

        public void setHeight(int height) {
            iHeight = height;
        }

        public void setSize(int width, int height) {
            setWidth(width);
            setHeight(height);
        }

        public void setText(String text) {
            iText = text;
        }

        public void setLeft(int left) {
            iLeft = left;
        }

        public void setRight(int right) {
            iRight = right;
        }

        public void setBottom(int bottom) {
            iBottom = bottom;
        }

        public void setTop(int top) {
            iTop = top;
        }

        public void setColor(String color) {
            iColor = color;
        }

        public void setBackgroundColor(String color) {
            iBgColor = color;
        }

        public void setItalics(boolean italics) {
            iItalics = italics;
        }

        public void add(P p) {
            iContent.add(p);
        }

        public void add(P p, int left, int top) {
            p.iTop = top;
            p.iLeft = left;
            iContent.add(p);
        }

        public int getLeft() {
            if (iLeft != null)
                return iLeft;
            if (iRight != null)
                return getWidth() - iRight;
            return 0;
        }

        public int getTop() {
            if (iTop != null)
                return iTop;
            if (iBottom != null)
                return getHeight() - iBottom;
            return 0;
        }

        public int getWidth() {
            if (iWidth != null)
                return iWidth;
            int width = 0;
            for (P p : iContent) {
                int w = p.getLeft() + p.getWidth();
                if (w > width)
                    width = w;
            }
            return width;
        }

        public int getHeight() {
            if (iHeight != null)
                return iHeight;
            int height = 0;
            for (P p : iContent) {
                int h = p.getTop() + p.getHeight();
                if (h > height)
                    height = h;
            }
            return height;
        }

        public boolean hasText() {
            return iText != null && !iText.isEmpty();
        }

        public String getText() {
            return iText;
        }

        public String getStyle() {
            return iStyle;
        }

        public boolean isStyle(String... style) {
            for (String s : style) {
                if (s.equals(iStyle))
                    return true;
            }
            return false;
        }

        protected void print(PdfContentByte canvas, int x, int y, int pageHeight, P parent)
                throws DocumentException {
            if (isStyle("unitime-TimetableGrid")) {
                setHeight(getHeight() - 2);
                setWidth(getWidth() - 1);
            }
            if (isStyle("vertical-separator")) {
                setHeight(parent.getHeight());
                setHeight(getHeight() - 1);
            }
            if (isStyle("horizontal-separator")) {
                setWidth(parent.getWidth());
                setWidth(getWidth() - 1);
            }
            Rectangle rect = new Rectangle(x + getLeft(), pageHeight - (y + getTop()),
                    x + getLeft() + getWidth() - 1, pageHeight - (y + getTop() + getHeight() - 1));
            if (parent != null && parent.isStyle("meeting")) {
                rect.setBackgroundColor(Color.WHITE);
            }
            if (iBgColor != null) {
                Pattern p = Pattern.compile("rgb\\(([0-9]+),([0-9]+),([0-9]+)\\)");
                Matcher m = p.matcher(iBgColor);
                if (m.matches()) {
                    rect.setBackgroundColor(new Color(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
                            Integer.parseInt(m.group(3))));
                }
            }
            if (parent != null && parent.isStyle("meeting")) {
                if (parent.iBgColor != null) {
                    Pattern p = Pattern.compile("rgb\\(([0-9]+),([0-9]+),([0-9]+)\\)");
                    Matcher m = p.matcher(parent.iBgColor);
                    if (m.matches()) {
                        Color color = new Color(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
                                Integer.parseInt(m.group(3)));
                        if (isStyle("header"))
                            rect.setBackgroundColor(color.darker());
                        else
                            rect.setBackgroundColor(color);
                    }
                } else {
                    Color color = new Color(240, 240, 240);
                    if (isStyle("header"))
                        rect.setBackgroundColor(color.darker());
                    else
                        rect.setBackgroundColor(color);
                }
            }
            if (rect.getBackgroundColor() != null)
                canvas.rectangle(rect);

            if (hasText()) {
                boolean bold = isStyle("grid-name", "horizontal-header", "vertical-header");
                if (isStyle("header") && parent.isStyle("meeting"))
                    bold = true;
                boolean italics = iItalics;
                int alignment = Element.ALIGN_CENTER;
                Font font = PdfFont.getSmallFont(bold, italics);
                if (iColor != null) {
                    Pattern p = Pattern.compile("rgb\\(([0-9]+),([0-9]+),([0-9]+)\\)");
                    Matcher m = p.matcher(iColor);
                    if (m.matches()) {
                        font.setColor(new Color(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
                                Integer.parseInt(m.group(3))));
                    }
                }
                int idx = 0;
                for (String line : getText().split("\\n")) {
                    Phrase p = new Phrase(line, font);
                    ColumnText ct = new ColumnText(canvas);
                    ct.setSimpleColumn(p, x + getLeft() + 2, pageHeight - (y + getTop() + 2 + sLineHeight * idx),
                            x + getLeft() + getWidth() - 4,
                            pageHeight - (y + getTop() + 2 + sLineHeight * idx + 15f), 8f, alignment);
                    ct.go();
                    idx++;
                }
            }

            if (isStyle("footer") && parent.isStyle("meeting")) {
                canvas.setColorStroke(Color.BLACK);
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop() + getHeight() - 1));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop() + getHeight() - 1));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop()));
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.closePathStroke();
            }

            if (isStyle("header") && parent.isStyle("meeting")) {
                canvas.setColorStroke(Color.BLACK);
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop() + getHeight()));
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.closePathStroke();
            }
            if (isStyle("vertical-separator")) {
                canvas.setColorStroke(Color.GRAY);
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.closePathStroke();
            }
            if (isStyle("horizontal-separator")) {
                canvas.setColorStroke(Color.GRAY);
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft() + getWidth(), pageHeight - (y + getTop()));
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.closePathStroke();
            }
            if (isStyle("header-interval")) {
                canvas.setColorStroke(Color.GRAY);
                canvas.moveTo(x + getLeft() + getWidth(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.moveTo(x + getLeft() + getWidth(), pageHeight - (y + getTop()));
                canvas.closePathStroke();
            }
            if (isStyle("unitime-TimetableGrid")) {
                canvas.setColorStroke(Color.GRAY);
                canvas.moveTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop()));
                canvas.lineTo(x + getLeft() + getWidth() - 1, pageHeight - (y + getTop() + getHeight()));
                canvas.lineTo(x + getLeft(), pageHeight - (y + getTop() + getHeight()));
                canvas.closePathStroke();
            }

            for (P p : iContent) {
                p.print(canvas, x + getLeft(), y + getTop(), pageHeight, this);
            }
        }
    }

    protected static class TimetableGrid extends P {
        private List<Meeting> iMeetings = new ArrayList<Meeting>();
        private List<Background> iBackbrounds = new ArrayList<Background>();
        private int iCellWidth;

        public TimetableGrid(FilterInterface filter, final TimetableGridModel model, int index, int pageWidth,
                int weekOffset, boolean showHeader) {
            super("unitime-TimetableGrid");

            int displayMode = Integer.valueOf(filter.getParameterValue("dispMode", "0"));
            boolean hasDay[] = { true, true, true, true, true, false, false };
            String days = filter.getParameterValue("days");
            int nrDays = 0;
            if (days != null && days.length() == 7 && days.indexOf('1') >= 0) {
                for (int i = 0; i < 7; i++)
                    if (days.charAt(i) == '1') {
                        hasDay[i] = true;
                        nrDays++;
                    } else {
                        hasDay[i] = false;
                    }
            }
            int nrTimes = 20;
            int startSlot = 90;
            int step = 6;
            String[] times = filter.getParameterValue("times", "90|222|6").split("\\|");
            if (times != null && times.length == 3) {
                startSlot = Integer.parseInt(times[0]);
                step = Integer.parseInt(times[2]);
                nrTimes = (Integer.parseInt(times[1]) - startSlot) / step;
            }
            int endSlot = startSlot + step * nrTimes;
            boolean showPreferences = "1".equals(filter.getParameterValue("showPreferences"));
            boolean showInstructors = "1".equals(filter.getParameterValue("showInstructors"));
            boolean showTimes = "1".equals(filter.getParameterValue("showTimes"));
            boolean showRoom = !"0".equals(filter.getParameterValue("resource"));
            boolean showDate = "-100".equals(filter.getParameterValue("weeks"));
            String comment = getComment(model);

            if (displayMode == 0) {
                int headerLines = (showHeader ? 1 : 0);
                int nrLines = 0;
                int[] dayIndex = new int[7];
                int x = 0;
                int[] dayLines = new int[7];
                for (int i = 0; i < 7; i++) {
                    int d = (i + weekOffset) % 7;
                    dayLines[d] = model.getNrLines(d, 2);
                    if (hasDay[d] && nrLines < dayLines[d])
                        nrLines = dayLines[d];
                    dayIndex[d] = x;
                    if (hasDay[d])
                        x++;
                }
                iCellWidth = (pageWidth - sHeaderWidth) / (nrDays * nrTimes);
                if (iCellWidth < 50)
                    iCellWidth = 50;
                setSize(sHeaderWidth + (nrDays * nrTimes) * iCellWidth + 2 * nrDays,
                        (headerLines + nrLines) * sLineHeight + 1);

                if (headerLines > 0) {
                    P name = new P("grid-name");
                    name.setSize(sHeaderWidth, headerLines * sLineHeight);
                    for (int i = 0; i < 7; i++) {
                        int d = (i + weekOffset) % 7;
                        if (!hasDay[d])
                            continue;
                        name.setText(CONSTANTS.longDays()[d]);
                        break;
                    }
                    add(name);
                }

                P verticalHeader = new P("vertical-header");
                verticalHeader.setSize(sHeaderWidth, nrLines * sLineHeight + 1);
                add(verticalHeader, 0, headerLines * sLineHeight);

                P panel = new P("timetable-panel");
                panel.setSize((nrDays * nrTimes) * iCellWidth + 2 * nrDays, nrLines * sLineHeight + 1);
                if (headerLines > 0) {
                    P horizontalHeader = new P("horizontal-header");
                    horizontalHeader.setSize((nrDays * nrTimes) * iCellWidth + 2 * nrDays,
                            headerLines * sLineHeight);
                    add(horizontalHeader, sHeaderWidth, 0);

                    for (int i = 0; i < 7; i++) {
                        int d = (i + weekOffset) % 7;
                        if (!hasDay[d])
                            continue;
                        for (int t = 0; t < nrTimes; t++) {
                            int j = dayIndex[d] * nrTimes + t;
                            P hi = new P("header-interval");
                            hi.setSize(iCellWidth, headerLines * sLineHeight);
                            hi.setText(slot2time(startSlot + t * step));
                            horizontalHeader.add(hi, j * iCellWidth + 2 * dayIndex[d], 0);
                            if (t == 0 && dayIndex[d] > 0) {
                                P vs = new P("vertical-double-separator");
                                panel.add(vs, j * iCellWidth + 2 * dayIndex[d] - 2, 0);
                                P vs2 = new P("vertical-double-separator");
                                horizontalHeader.add(vs2, j * iCellWidth + 2 * dayIndex[d] - 2, 0);
                            } else {
                                P vs = new P("vertical-separator");
                                panel.add(vs, j * iCellWidth + 2 * dayIndex[d], 0);
                            }
                        }
                    }
                    P lastVShead = new P("vertical-separator");
                    lastVShead.setRight(0);
                    lastVShead.setTop(0);
                    horizontalHeader.add(lastVShead);
                } else {
                    for (int d = 0; d < 7; d++) {
                        if (!hasDay[d])
                            continue;
                        for (int t = 0; t < nrTimes; t++) {
                            int i = dayIndex[d] * nrTimes + t;
                            if (t == 0 && dayIndex[d] > 0) {
                                P vs = new P("vertical-double-separator");
                                panel.add(vs, i * iCellWidth + 2 * dayIndex[d] - 2, 0);
                            } else {
                                P vs = new P("vertical-separator");
                                panel.add(vs, i * iCellWidth + 2 * dayIndex[d], 0);
                            }
                        }
                    }
                }

                add(panel, sHeaderWidth, headerLines * sLineHeight);
                P lastVSpan = new P("vertical-separator");
                lastVSpan.setRight(0);
                lastVSpan.setTop(0);
                panel.add(lastVSpan);

                P hs = new P("horizontal-separator");
                panel.add(hs, 0, 0);
                final P hi = new P("header-interval");
                hi.setSize(sHeaderWidth, nrLines * sLineHeight);
                hi.setText(model.getName() + (comment != null && !comment.isEmpty() ? "\n" + comment : ""));
                if (model.hasNameColor())
                    hi.setColor(model.getNameColor());
                verticalHeader.add(hi, 0, 0);

                P lastHSpan = new P("horizontal-separator");
                lastHSpan.setLeft(0);
                lastHSpan.setBottom(0);
                panel.add(lastHSpan);
                P lastHShead = new P("horizontal-separator");
                lastHShead.setLeft(0);
                lastHShead.setBottom(0);
                verticalHeader.add(lastHShead);

                for (TimetableGridBackground cell : model.getBackgrounds()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Background b = new Background(cell);
                    iBackbrounds.add(b);
                    b.setHeight(nrLines * sLineHeight);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    b.setWidth(stop * iCellWidth / step - start * iCellWidth / step);
                    panel.add(b, dayIndex[cell.getDay()] * nrTimes * iCellWidth
                            + (start - startSlot) * iCellWidth / step + 2 * dayIndex[cell.getDay()], 0);
                }

                for (TimetableGridCell cell : model.getCells()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Meeting m = new Meeting(cell, showRoom, showInstructors, showTimes, showPreferences, showDate);
                    iMeetings.add(m);
                    int lines = cell.getNrLines();
                    if (dayLines[cell.getDay()] < nrLines
                            && (cell.getIndex() + cell.getNrLines() == dayLines[cell.getDay()])) {
                        lines += nrLines - dayLines[cell.getDay()];
                    }
                    m.setHeight(1 + lines * sLineHeight);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    m.setWidth(
                            1 + (stop - startSlot) * iCellWidth / step - (start - startSlot) * iCellWidth / step);
                    panel.add(
                            m, dayIndex[cell.getDay()] * nrTimes * iCellWidth
                                    + (start - startSlot) * iCellWidth / step + 2 * dayIndex[cell.getDay()],
                            cell.getIndex() * sLineHeight);
                }
            } else if (displayMode == 1) {
                int nrLines = 0;
                int[] dayIndex = new int[8];
                int[] toDayIdx = new int[7];
                for (int i = 0; i < 7; i++) {
                    int d = (i + weekOffset) % 7;
                    dayIndex[i] = nrLines;
                    toDayIdx[d] = i;
                    if (hasDay[d])
                        nrLines += model.getNrLines(d, 2);
                }
                dayIndex[7] = nrLines;

                int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
                iCellWidth = (pageWidth - sHeaderWidth) / nrTimes;
                setSize(sHeaderWidth + nrTimes * iCellWidth + 2, (headerLines + nrLines) * sLineHeight + 2);

                final P name = new P("grid-name");
                name.setSize(sHeaderWidth, headerLines * sLineHeight);
                name.setText(model.getName() + (comment != null && !comment.isEmpty() ? "\n" + comment : ""));
                if (model.hasNameColor())
                    name.setColor(model.getNameColor());
                add(name);
                P verticalHeader = new P("vertical-header");
                verticalHeader.setSize(sHeaderWidth, nrLines * sLineHeight + 1);
                add(verticalHeader, 0, headerLines * sLineHeight);

                P horizontalHeader = new P("horizontal-header");
                horizontalHeader.setSize(nrTimes * iCellWidth + 1, headerLines * sLineHeight);
                add(horizontalHeader, sHeaderWidth, 0);

                P panel = new P("timetable-panel");
                panel.setSize(nrTimes * iCellWidth + 1, nrLines * sLineHeight + 1);
                add(panel, sHeaderWidth, headerLines * sLineHeight);

                for (int i = 0; i < nrTimes; i++) {
                    P hi = new P("header-interval");
                    hi.setSize(iCellWidth, headerLines * sLineHeight);
                    hi.setText(slot2time(startSlot + i * step));
                    horizontalHeader.add(hi, i * iCellWidth, 0);

                    P vs = new P("vertical-separator");
                    panel.add(vs, i * iCellWidth, 0);
                }
                P lastVSpan = new P("vertical-separator");
                lastVSpan.setRight(0);
                lastVSpan.setTop(0);
                panel.add(lastVSpan);
                P lastVShead = new P("vertical-separator");
                lastVShead.setRight(0);
                lastVShead.setTop(0);
                horizontalHeader.add(lastVShead);

                for (int i = 0; i < 7; i++) {
                    int d = (i + weekOffset) % 7;
                    if (!hasDay[d])
                        continue;
                    P hs = new P("horizontal-separator");
                    panel.add(hs, 0, sLineHeight * dayIndex[i]);

                    P hi = new P("header-interval");
                    hi.setSize(sHeaderWidth, (dayIndex[1 + i] - dayIndex[i]) * sLineHeight);
                    hi.setText(CONSTANTS.longDays()[d]);
                    verticalHeader.add(hi, 0, sLineHeight * dayIndex[i]);
                }
                P lastHSpan = new P("horizontal-separator");
                lastHSpan.setLeft(0);
                lastHSpan.setBottom(0);
                panel.add(lastHSpan);
                P lastHShead = new P("horizontal-separator");
                lastHShead.setLeft(0);
                lastHShead.setBottom(0);
                verticalHeader.add(lastHShead);

                for (TimetableGridBackground cell : model.getBackgrounds()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Background b = new Background(cell);
                    iBackbrounds.add(b);
                    int i = toDayIdx[cell.getDay()];
                    b.setHeight((dayIndex[i + 1] - dayIndex[i]) * sLineHeight);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    b.setWidth(stop * iCellWidth / step - start * iCellWidth / step);
                    panel.add(b, (start - startSlot) * iCellWidth / step, dayIndex[i] * sLineHeight);
                }

                for (TimetableGridCell cell : model.getCells()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Meeting m = new Meeting(cell, showRoom, showInstructors, showTimes, showPreferences, showDate);
                    iMeetings.add(m);
                    m.setHeight(1 + cell.getNrLines() * sLineHeight);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    m.setWidth(
                            1 + (stop - startSlot) * iCellWidth / step - (start - startSlot) * iCellWidth / step);
                    panel.add(m, (start - startSlot) * iCellWidth / step,
                            (dayIndex[toDayIdx[cell.getDay()]] + cell.getIndex()) * sLineHeight);
                }
            } else if (displayMode == 2) {
                int nrColumns = 0;
                int[] colIndex = new int[8];
                int[] toDayIdx = new int[7];
                for (int i = 0; i < 7; i++) {
                    int d = (i + weekOffset) % 7;
                    colIndex[i] = nrColumns;
                    toDayIdx[d] = i;
                    if (hasDay[d])
                        nrColumns += model.getNrLines(d, 1);
                }
                colIndex[7] = nrColumns;
                int linesPerTime = 2;

                int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
                iCellWidth = (pageWidth - sHeaderWidth) / nrColumns;
                setSize(sHeaderWidth + nrColumns * iCellWidth + 2,
                        (headerLines + linesPerTime * nrTimes) * sLineHeight + 2);

                final P name = new P("grid-name");
                name.setSize(sHeaderWidth, headerLines * sLineHeight);
                name.setText(model.getName() + (comment != null && !comment.isEmpty() ? "\n" + comment : ""));
                if (model.hasNameColor())
                    name.setColor(model.getNameColor());
                add(name);

                P verticalHeader = new P("vertical-header");
                verticalHeader.setSize(sHeaderWidth, linesPerTime * nrTimes * sLineHeight + 1);
                add(verticalHeader, 0, headerLines * sLineHeight);

                P horizontalHeader = new P("horizontal-header");
                horizontalHeader.setSize(nrColumns * iCellWidth + 1, headerLines * sLineHeight);
                add(horizontalHeader, sHeaderWidth, 0);

                P panel = new P("timetable-panel");
                panel.setSize(nrColumns * iCellWidth + 1, linesPerTime * nrTimes * sLineHeight + 1);
                add(panel, sHeaderWidth, headerLines * sLineHeight);

                for (int i = 0; i < 7; i++) {
                    int d = (i + weekOffset) % 7;
                    if (!hasDay[d])
                        continue;
                    P hi = new P("header-interval");
                    hi.setSize(iCellWidth * model.getNrLines(d, 1), headerLines * sLineHeight);
                    hi.setText(CONSTANTS.longDays()[d]);
                    horizontalHeader.add(hi, colIndex[i] * iCellWidth, 0);

                    P vs = new P("vertical-separator");
                    panel.add(vs, colIndex[i] * iCellWidth, 0);
                }
                P lastVSpan = new P("vertical-separator");
                lastVSpan.setRight(0);
                lastVSpan.setTop(0);
                panel.add(lastVSpan);
                P lastVShead = new P("vertical-separator");
                lastVShead.setRight(0);
                lastVShead.setTop(0);
                horizontalHeader.add(lastVShead);

                for (int i = 0; i < nrTimes; i++) {
                    P hs = new P("horizontal-separator");
                    panel.add(hs, 0, sLineHeight * linesPerTime * i);

                    P hi = new P("header-interval");
                    hi.setSize(sHeaderWidth, linesPerTime * sLineHeight);
                    hi.setText(slot2time(startSlot + step * i));
                    verticalHeader.add(hi, 0, sLineHeight * linesPerTime * i);
                }
                P lastHSpan = new P("horizontal-separator");
                lastHSpan.setLeft(0);
                lastHSpan.setBottom(0);
                panel.add(lastHSpan);
                P lastHShead = new P("horizontal-separator");
                lastHShead.setLeft(0);
                lastHShead.setBottom(0);
                verticalHeader.add(lastHShead);

                for (TimetableGridBackground cell : model.getBackgrounds()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Background b = new Background(cell);
                    iBackbrounds.add(b);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    b.setHeight(
                            stop * sLineHeight * linesPerTime / step - start * sLineHeight * linesPerTime / step);
                    int i = toDayIdx[cell.getDay()];
                    b.setWidth((colIndex[1 + i] - colIndex[i]) * iCellWidth);
                    panel.add(b, colIndex[i] * iCellWidth, (start - startSlot) * sLineHeight * linesPerTime / step);
                }

                for (TimetableGridCell cell : model.getCells()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    Meeting m = new Meeting(cell, showRoom, showInstructors, showTimes, showPreferences, showDate);
                    iMeetings.add(m);
                    int start = cell.getSlot();
                    int stop = cell.getSlot() + cell.getLength();
                    if (start < startSlot)
                        start = startSlot;
                    if (stop > endSlot)
                        stop = endSlot;
                    m.setHeight(1 + (stop - startSlot) * sLineHeight * linesPerTime / step
                            - (start - startSlot) * sLineHeight * linesPerTime / step);
                    m.setWidth(1 + iCellWidth * cell.getNrLines());
                    panel.add(m, (colIndex[toDayIdx[cell.getDay()]] + cell.getIndex()) * iCellWidth,
                            (start - startSlot) * sLineHeight * linesPerTime / step);
                }
            } else if (displayMode == 3) {
                int nrLines = 0;
                int[] dayIndex = new int[366];
                for (int d = 0; d < 365; d++) {
                    dayIndex[d] = nrLines;
                    int date = d + model.getFirstSessionDay();
                    if (model.getFirstDay() >= 0 && (date < model.getFirstDay() || date > model.getFirstDay() + 6))
                        continue;
                    int day = d % 7;
                    if (hasDay[day] && model.hasDate(day, date)) {
                        nrLines += model.getNrDateLines(day, date, 2);
                    }
                }
                dayIndex[365] = nrLines;

                int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
                iCellWidth = (pageWidth - sHeaderWidth) / nrTimes;
                setSize(sHeaderWidth + nrTimes * iCellWidth + 2, (headerLines + nrLines) * sLineHeight + 2);

                final P name = new P("grid-name");
                name.setSize(sHeaderWidth, headerLines * sLineHeight);
                name.setText(model.getName() + (comment != null && !comment.isEmpty() ? "\n" + comment : ""));
                if (model.hasNameColor())
                    name.setColor(model.getNameColor());
                add(name);

                P verticalHeader = new P("vertical-header");
                verticalHeader.setSize(sHeaderWidth, nrLines * sLineHeight + 1);
                add(verticalHeader, 0, headerLines * sLineHeight);

                P horizontalHeader = new P("horizontal-header");
                horizontalHeader.setSize(nrTimes * iCellWidth + 1, headerLines * sLineHeight);
                add(horizontalHeader, sHeaderWidth, 0);

                P panel = new P("timetable-panel");
                panel.setSize(nrTimes * iCellWidth + 1, nrLines * sLineHeight + 1);
                add(panel, sHeaderWidth, headerLines * sLineHeight);

                for (int i = 0; i < nrTimes; i++) {
                    P hi = new P("header-interval");
                    hi.setSize(iCellWidth, headerLines * sLineHeight);
                    hi.setText(slot2time(startSlot + i * step));
                    horizontalHeader.add(hi, i * iCellWidth, 0);

                    P vs = new P("vertical-separator");
                    panel.add(vs, i * iCellWidth, 0);
                }
                P lastVSpan = new P("vertical-separator");
                lastVSpan.setRight(0);
                lastVSpan.setTop(0);
                panel.add(lastVSpan);
                P lastVShead = new P("vertical-separator");
                lastVShead.setRight(0);
                lastVShead.setTop(0);
                horizontalHeader.add(lastVShead);

                for (int d = 0; d < 365; d++) {
                    int date = d + model.getFirstSessionDay();
                    if (model.getFirstDay() >= 0 && (date < model.getFirstDay() || date > model.getFirstDay() + 6))
                        continue;
                    int day = d % 7;
                    if (!hasDay[day] || !model.hasDate(day, date))
                        continue;
                    P hs = new P("horizontal-separator");
                    panel.add(hs, 0, sLineHeight * dayIndex[d]);

                    P hi = new P("header-interval");
                    hi.setSize(sHeaderWidth, (dayIndex[1 + d] - dayIndex[d]) * sLineHeight);
                    hi.setText(sDateFormatMeeting.format(getDate(model.getFirstDate(), d)));
                    verticalHeader.add(hi, 0, sLineHeight * dayIndex[d]);
                }
                P lastHSpan = new P("horizontal-separator");
                lastHSpan.setLeft(0);
                lastHSpan.setBottom(0);
                panel.add(lastHSpan);
                P lastHShead = new P("horizontal-separator");
                lastHShead.setLeft(0);
                lastHShead.setBottom(0);
                verticalHeader.add(lastHShead);

                for (TimetableGridBackground cell : model.getBackgrounds()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    for (int d = 0; d < 365; d++) {
                        int date = d + model.getFirstSessionDay();
                        int day = d % 7;
                        if (model.getFirstDay() >= 0
                                && (date < model.getFirstDay() || date > model.getFirstDay() + 6))
                            continue;
                        if (cell.getDay() == day) {
                            Background b = new Background(cell);
                            iBackbrounds.add(b);
                            b.setHeight((dayIndex[1 + d] - dayIndex[d]) * sLineHeight);
                            int start = cell.getSlot();
                            int stop = cell.getSlot() + cell.getLength();
                            if (start < startSlot)
                                start = startSlot;
                            if (stop > endSlot)
                                stop = endSlot;
                            b.setWidth(stop * iCellWidth / step - start * iCellWidth / step);
                            panel.add(b, (start - startSlot) * iCellWidth / step, dayIndex[d] * sLineHeight);
                        }
                    }
                }

                for (TimetableGridCell cell : model.getCells()) {
                    if (!hasDay[cell.getDay()])
                        continue;
                    if (cell.getSlot() + cell.getLength() <= startSlot)
                        continue;
                    if (cell.getSlot() >= startSlot + step * nrTimes)
                        continue;
                    for (int d = 0; d < 365; d++) {
                        int date = d + model.getFirstSessionDay();
                        if (model.getFirstDay() >= 0
                                && (date < model.getFirstDay() || date > model.getFirstDay() + 6))
                            continue;
                        int day = d % 7;
                        if (cell.getDay() == day && cell.hasDate(date)) {
                            Meeting m = new Meeting(cell, showRoom, showInstructors, showTimes, showPreferences,
                                    showDate);
                            iMeetings.add(m);
                            m.setHeight(1 + cell.getNrLines(date) * sLineHeight);
                            int start = cell.getSlot();
                            int stop = cell.getSlot() + cell.getLength();
                            if (start < startSlot)
                                start = startSlot;
                            if (stop > endSlot)
                                stop = endSlot;
                            m.setWidth(1 + (stop - startSlot) * iCellWidth / step
                                    - (start - startSlot) * iCellWidth / step);
                            panel.add(m, (start - startSlot) * iCellWidth / step,
                                    (dayIndex[d] + cell.getIndex(date)) * sLineHeight);
                        }
                    }
                }
            }
        }
    }

    protected static float textWidth(Font font, TimetableGridCell cell, boolean showRoom, boolean showInstructor,
            boolean showTime, boolean showPreference, boolean showDate) {
        float width = 0;
        if (cell.getNrNames() > 0) {
            for (String name : cell.getNames())
                width = Math.max(width, font.getBaseFont().getWidthPoint(name, font.getSize()));
        }
        if (showTime && cell.hasTime())
            width = Math.max(width, font.getBaseFont().getWidthPoint(cell.getTime(), font.getSize()));
        if (showDate && cell.hasDate())
            width = Math.max(width, font.getBaseFont().getWidthPoint(cell.getDate(), font.getSize()));
        if (showRoom && cell.getNrRooms() > 0)
            for (String room : cell.getRooms())
                width = Math.max(width, font.getBaseFont().getWidthPoint(room, font.getSize()));
        if (showInstructor && cell.getNrInstructors() > 0)
            for (String instructor : cell.getInstructors())
                width = Math.max(width, font.getBaseFont().getWidthPoint(instructor, font.getSize()));
        if (showPreference && cell.hasPreference())
            width = Math.max(width, font.getBaseFont()
                    .getWidthPoint(cell.getPreference().replaceAll("\\<[^>]*>", ""), font.getSize()));
        return width;
    }

    protected int pageWidth(FilterInterface filter, final TimetableGridModel model, int weekOffset) {
        int displayMode = Integer.valueOf(filter.getParameterValue("dispMode", "0"));
        boolean hasDay[] = { true, true, true, true, true, false, false };
        String days = filter.getParameterValue("days");
        if (days != null && days.length() == 7 && days.indexOf('1') >= 0) {
            for (int i = 0; i < 7; i++)
                if (days.charAt(i) == '1') {
                    hasDay[i] = true;
                } else {
                    hasDay[i] = false;
                }
        }
        int nrTimes = 20;
        int startSlot = 90;
        int step = 6;
        String[] times = filter.getParameterValue("times", "90|222|6").split("\\|");
        if (times != null && times.length == 3) {
            startSlot = Integer.parseInt(times[0]);
            step = Integer.parseInt(times[2]);
            nrTimes = (Integer.parseInt(times[1]) - startSlot) / step;
        }
        int endSlot = startSlot + step * nrTimes;
        boolean showPreferences = "1".equals(filter.getParameterValue("showPreferences"));
        boolean showInstructors = "1".equals(filter.getParameterValue("showInstructors"));
        boolean showTimes = "1".equals(filter.getParameterValue("showTimes"));
        boolean showRoom = !"0".equals(filter.getParameterValue("resource"));
        boolean showDate = "-100".equals(filter.getParameterValue("weeks"));
        Font font = PdfFont.getSmallFont();

        int nrColumns = 0;
        for (int i = 0; i < 7; i++) {
            int d = (i + weekOffset) % 7;
            if (hasDay[d])
                nrColumns += model.getNrLines(d, 1);
        }

        float widthPerSlot = 50f / step;
        float widthPerColumn = 50f;
        for (TimetableGridCell cell : model.getCells()) {
            if (!hasDay[cell.getDay()])
                continue;
            if (cell.getSlot() + cell.getLength() <= startSlot)
                continue;
            if (cell.getSlot() >= startSlot + step * nrTimes)
                continue;
            float textWidth = textWidth(font, cell, showRoom, showInstructors, showTimes, showPreferences, showDate)
                    + 10f;
            int start = cell.getSlot();
            int stop = cell.getSlot() + cell.getLength();
            if (start < startSlot)
                start = startSlot;
            if (stop > endSlot)
                stop = endSlot;
            widthPerSlot = Math.max(widthPerSlot, textWidth / (stop - start));
            widthPerColumn = Math.max(widthPerColumn, textWidth);
        }

        if (displayMode == 0) {
            return Math.round(sHeaderWidth + step * nrTimes * widthPerSlot);
        } else if (displayMode == 2) {
            return Math.round(sHeaderWidth + nrColumns * widthPerColumn);
        } else {
            return Math.min(1500, Math.round(sHeaderWidth + step * nrTimes * widthPerSlot));
        }
    }

    protected int pageHeight(FilterInterface filter, final TimetableGridModel model, int weekOffset,
            boolean showHeader) {
        int displayMode = Integer.valueOf(filter.getParameterValue("dispMode", "0"));
        boolean hasDay[] = { true, true, true, true, true, false, false };
        String days = filter.getParameterValue("days");
        if (days != null && days.length() == 7 && days.indexOf('1') >= 0) {
            for (int i = 0; i < 7; i++)
                if (days.charAt(i) == '1') {
                    hasDay[i] = true;
                } else {
                    hasDay[i] = false;
                }
        }
        int nrTimes = 20;
        int startSlot = 90;
        int step = 6;
        String[] times = filter.getParameterValue("times", "90|222|6").split("\\|");
        if (times != null && times.length == 3) {
            startSlot = Integer.parseInt(times[0]);
            step = Integer.parseInt(times[2]);
            nrTimes = (Integer.parseInt(times[1]) - startSlot) / step;
        }
        String comment = getComment(model);

        if (displayMode == 0) {
            int headerLines = (showHeader ? 1 : 0);
            int nrLines = 0;
            for (int i = 0; i < 7; i++) {
                int d = (i + weekOffset) % 7;
                int dayLines = model.getNrLines(d, 2);
                if (hasDay[d] && nrLines < dayLines)
                    nrLines = dayLines;
            }
            return (headerLines + nrLines) * sLineHeight + 1;
        } else if (displayMode == 1) {
            int nrLines = 0;
            for (int i = 0; i < 7; i++) {
                int d = (i + weekOffset) % 7;
                if (hasDay[d])
                    nrLines += model.getNrLines(d, 2);
            }
            int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
            return (headerLines + nrLines) * sLineHeight + 2;
        } else if (displayMode == 2) {
            int linesPerTime = 2;
            int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
            return (headerLines + linesPerTime * nrTimes) * sLineHeight + 2;
        } else if (displayMode == 3) {
            int nrLines = 0;
            for (int d = 0; d < 365; d++) {
                int date = d + model.getFirstSessionDay();
                if (model.getFirstDay() >= 0 && (date < model.getFirstDay() || date > model.getFirstDay() + 6))
                    continue;
                int day = d % 7;
                if (hasDay[day] && model.hasDate(day, date)) {
                    nrLines += model.getNrDateLines(day, date, 2);
                }
            }

            int headerLines = 1 + (comment != null && !comment.isEmpty() ? 1 : 0);
            return (headerLines + nrLines) * sLineHeight + 2;
        }
        return 0;
    }

    protected static class Meeting extends P {
        private TimetableGridCell iCell;
        private P iHeader, iFooter;

        private Meeting(TimetableGridCell cell, boolean showRoom, boolean showInstructor, boolean showTime,
                boolean showPreference, boolean showDate) {
            super("meeting");
            iCell = cell;

            if (cell.hasBackground())
                setBackgroundColor(cell.getBackground());

            iHeader = new P("header");
            iHeader.setHeight(sLineHeight * cell.getNrNames());
            iHeader.setText(cell.getName("\n"));
            if (cell.isItalics())
                iHeader.setItalics(true);
            add(iHeader, 0, 0);

            iFooter = new P("footer");
            String notes = "";
            if (showTime && cell.hasTime())
                notes += (notes.isEmpty() ? "" : "\n") + cell.getTime();
            if (showDate && cell.hasDate())
                notes += (notes.isEmpty() ? "" : "\n") + cell.getDate();
            if (showRoom && cell.getNrRooms() > 0)
                notes += (notes.isEmpty() ? "" : "\n") + cell.getRoom("\n");
            if (showInstructor && cell.getNrInstructors() > 0)
                notes += (notes.isEmpty() ? "" : "\n") + cell.getInstructor("\n");
            if (showPreference && cell.hasPreference())
                notes += (notes.isEmpty() ? "" : "\n") + cell.getPreference().replaceAll("\\<[^>]*>", "");
            iFooter.setText(notes);
            add(iFooter, 0, iHeader.getHeight());

            // setText(cell.getName("\n") + "\n" + notes);
        }

        @Override
        public void setHeight(int height) {
            super.setHeight(height);
            iFooter.setHeight(height - iHeader.getHeight());
        }

        @Override
        public void setWidth(int width) {
            super.setWidth(width);
            iHeader.setWidth(width);
            iFooter.setWidth(width);
        }

        public TimetableGridCell getCell() {
            return iCell;
        }
    }

    protected static class Background extends P {
        private TimetableGridBackground iBackground;

        public Background(TimetableGridBackground background) {
            super("background");
            iBackground = background;

            if (background.hasBackground())
                setBackgroundColor(background.getBackground());
        }

        public TimetableGridBackground getBackground() {
            return iBackground;
        }
    }

    protected static String getComment(TimetableGridModel model) {
        if (model.getResourceType() == 0) {
            return "(" + model.getSize() + ", " + sUtilizationFormat.format(model.getUtilization()) + ")";
        } else if (model.getResourceType() >= 2) {
            return "(" + model.getSize() + ")";
        } else {
            return null;
        }
    }

    @SuppressWarnings("deprecation")
    protected static Date getDate(Date firstDate, int date) {
        Date ret = new Date(firstDate.getTime());
        ret.setDate(ret.getDate() + date);
        return ret;
    }

    public static String slot2time(int slot) {
        if (CONSTANTS.useAmPm()) {
            if (slot == 0)
                return CONSTANTS.timeMidnight();
            if (slot == 144)
                return CONSTANTS.timeNoon();
            if (slot == 288)
                return CONSTANTS.timeMidnightEnd();
        }
        int h = slot / 12;
        int m = 5 * (slot % 12);
        if (CONSTANTS.useAmPm())
            return (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + " "
                    + (h == 24 ? CONSTANTS.timeAm() : h >= 12 ? CONSTANTS.timePm() : CONSTANTS.timeAm());
        else
            return h + ":" + (m < 10 ? "0" : "") + m;
    }
}