package co.smartreceipts.android.workers.reports.pdf.misc; import java.io.IOException; import java.util.ArrayList; import java.util.List; import co.smartreceipts.android.model.Column; import co.smartreceipts.android.workers.reports.pdf.utils.PdfBoxUtils; import co.smartreceipts.android.workers.reports.pdf.fonts.PdfFontSpec; import co.smartreceipts.android.workers.reports.pdf.utils.HeavyHandedReplaceIllegalCharacters; public class ColumnWidthCalculator<DataType> { private static final float EPSILON = 0.00001f; private final List<DataType> mList; private final List<Column<DataType>> mColumns; private final float mAvailableWidth; private final float mCellPadding; private final PdfFontSpec mFontHeader; private final PdfFontSpec mFontContent; /** * @param list * @param columns * @param availableWidth * @param fontHeader * @param fontContent */ public ColumnWidthCalculator(List<DataType> list, List<Column<DataType>> columns, float availableWidth, float cellPadding, PdfFontSpec fontHeader, PdfFontSpec fontContent) { mList = list; mColumns = columns; mAvailableWidth = availableWidth; mFontHeader = fontHeader; mFontContent = fontContent; mCellPadding = cellPadding; } public float[] calculate() throws IOException, TooManyColumnsException { float availableWidthExcludingPadding = mAvailableWidth - 2 * mColumns.size() * mCellPadding; float[] widths = new float[mColumns.size()]; ArrayList<ColumnAttributes> attrs = new ArrayList<ColumnAttributes>(mColumns.size()); for (int i = 0; i < mColumns.size(); i++) { attrs.add(new ColumnAttributes(mColumns.get(i).getHeader(), mList, i)); } // TOO MANY COLUMNS CHECK // If columns do not fit with their min mWidth, abort float sumCheck = 0.0f; for (int i = 0; i < attrs.size(); i++) { float m = Math.max(attrs.get(i).mHeaderMinWidth, attrs.get(i).mContentMinWidth); sumCheck += m; } if (sumCheck > availableWidthExcludingPadding) { throw new TooManyColumnsException(); } // FIRST ATTEMPT // If all columns fit with their max mWidth, assign maxWidth // and then redistribute evenly for (int i = 0; i < attrs.size(); i++) { float m = Math.max(attrs.get(i).mHeaderMaxWidth, attrs.get(i).mContentMaxWidth); widths[i] = m + 2 * mCellPadding; } if (sum(widths) < mAvailableWidth) { return distributeExtraSpaceEvenly(widths); } // SECOND ATTEMPT // Wrap titles (maintaining content unwrapped) for (int i = 0; i < attrs.size(); i++) { // The second condition defensively wraps the title (not necessarily to the minimum // header width, but just up to the content's max width) if (attrs.get(i).mHeaderBreakable && attrs.get(i).mContentMaxWidth < attrs.get(i).mHeaderMaxWidth) { float m = Math.max(attrs.get(i).mHeaderMinWidth, attrs.get(i).mContentMaxWidth); widths[i] = m + 2 * mCellPadding; } } if (sum(widths) < mAvailableWidth) { return distributeExtraSpaceEvenly(widths); } // THIRD PASS // Wrap contents (and possibly further wrap title) boolean[] flex = new boolean[mColumns.size()]; for (int i = 0; i < attrs.size(); i++) { float m = Math.max(attrs.get(i).mContentMinWidth, attrs.get(i).mHeaderMinWidth); float newWidth = m + 2 * mCellPadding; if (Math.abs(newWidth - widths[i]) > EPSILON) { widths[i] = newWidth; flex[i] = true; } } if (sum(widths) < mAvailableWidth) { return distributeExtraSpaceOnlyToColumns(widths, flex); } throw new TooManyColumnsException(); } private float[] distributeExtraSpaceOnlyToColumns(float[] widths, boolean[] flex) { int nCols = 0; for (boolean b : flex) { if (b) { nCols++; } } float extraSpace = mAvailableWidth - sum(widths); for (int i = 0; i < widths.length; i++) { if (flex[i]) { widths[i] += extraSpace / nCols; } } return widths; } public float[] distributeExtraSpaceEvenly(float[] widths) { float extraSpace = mAvailableWidth - sum(widths); for (int j = 0; j < widths.length; j++) { widths[j] += extraSpace / mColumns.size(); } return widths; } private float sum(float[] widths) { float sum = 0.0f; for (float width : widths) { sum += width; } return sum; } private class ColumnAttributes { float mHeaderMinWidth; float mHeaderMaxWidth; float mContentMinWidth; float mContentMaxWidth; boolean mHeaderBreakable; public ColumnAttributes(String header, List<DataType> list, int i) throws IOException { mHeaderMaxWidth = PdfBoxUtils.getStringWidth(header, mFontHeader); mHeaderMinWidth = PdfBoxUtils.getMaxWordWidth(header, mFontHeader); float maxOfAllStringWidths = 0.0f; // the max string mWidth of all values (without breaking up the string) float minOfAllStringWidths = Float.MAX_VALUE; // the min string mWidth of all values (without breaking up the string) float maxOfMaxWordWidths = 0.0f; // the global max of the for (DataType dataType : list) { final String value = HeavyHandedReplaceIllegalCharacters.getSafeString(mColumns.get(i).getValue(dataType)); float vWidth = PdfBoxUtils.getStringWidth(value, mFontContent); float vMaxWordWidth = PdfBoxUtils.getMaxWordWidth(value, mFontContent); if (vWidth > maxOfAllStringWidths) { maxOfAllStringWidths = vWidth; } if (vWidth < minOfAllStringWidths) { minOfAllStringWidths = vWidth; } if (vMaxWordWidth > maxOfMaxWordWidths) { maxOfMaxWordWidths = vMaxWordWidth; } } mContentMaxWidth = maxOfAllStringWidths; mContentMinWidth = maxOfMaxWordWidths; mHeaderBreakable = header.contains(" "); } } }