// Copyright 2013-08-08 PlanBase Inc. & Glen Peterson
//
// Licensed 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 com.planbase.pdf.layoutmanager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents styled text kind of like a #Text node in HTML.
*/
public class Text implements Renderable {
private final TextStyle textStyle;
private final String text;
private final Map<Float,WrappedBlock> dims = new HashMap<Float,WrappedBlock>();
private final CellStyle.Align align = CellStyle.DEFAULT_ALIGN;
private static class WrappedRow {
String string;
XyDim rowDim;
public static WrappedRow of(String s, float x, float y) {
WrappedRow wr = new WrappedRow();
wr.string = s;
wr.rowDim = XyDim.of(x, y);
return wr;
}
}
private static class WrappedBlock {
List<WrappedRow> rows = new ArrayList<WrappedRow>();
XyDim blockDim;
}
public static final Text DEFAULT = new Text(null, "");
private Text(TextStyle s, String t) {
textStyle = s; text = t;
}
public static Text of(TextStyle style, String text) {
if (text == null) { text = ""; }
if ( "".equals(text) && (style == null) ) {
return DEFAULT;
}
return new Text(style, text);
}
public String text() { return text; };
public TextStyle style() { return textStyle; }
public int avgCharsForWidth(float width) {
return (int) ((width * 1220) / textStyle.avgCharWidth());
}
public float maxWidth() { return textStyle.stringWidthInDocUnits(text.trim()); }
private XyDim calcDimensionsForReal(final float maxWidth) {
// TODO: Make this show text even if the width is zero or less, just show one word per line.
if (maxWidth < 0) {
throw new IllegalArgumentException("Can't meaningfully wrap text with a negative width: " + maxWidth);
}
WrappedBlock wb = new WrappedBlock();
float x = 0;
float y = 0;
float maxX = x;
Text txt = this;
String row = PdfLayoutMgr.convertJavaStringToWinAnsi(txt.text());
String text = substrNoLeadingWhitespace(row, 0);
int charWidthGuess = txt.avgCharsForWidth(maxWidth);
while (text.length() > 0) {
int textLen = text.length();
// System.out.println("text=[" + text + "] len=" + textLen);
// Knowing the average width of a character lets us guess and generally be near
// the word where the line break will occur. Since the font reports a narrow average,
// (possibly due to the predominance of spaces in text) we widen it a little for a
// better first guess.
int idx = charWidthGuess;
if (idx > textLen) { idx = textLen; }
String substr = text.substring(0, idx);
float strWidth = textStyle.stringWidthInDocUnits(substr);
// System.out.println("(strWidth=" + strWidth + " < maxWidth=" + maxWidth + ") && (idx=" + idx + " < textLen=" + textLen + ")");
// If too short - find shortest string that is too long.
// int idx = idx;
// int maxTooShortIdx = -1;
while ( (strWidth < maxWidth) && (idx < textLen) ) {
// System.out.println("find shortest string that is too long");
// Consume any whitespace.
while ( (idx < textLen) &&
Character.isWhitespace(text.charAt(idx)) ) {
idx++;
}
// Find last non-whitespace character
while ( (idx < textLen) &&
!Character.isWhitespace(text.charAt(idx)) ) {
idx++;
}
// Test new width
substr = text.substring(0, idx);
strWidth = textStyle.stringWidthInDocUnits(substr);
}
idx--;
// System.out.println("(strWidth=" + strWidth + " > maxWidth=" + maxWidth + ") && (idx=" + idx + " > 0)");
// Too long. Find longest string that is short enough.
while ( (strWidth > maxWidth) && (idx > 0) ) {
// System.out.println("find longest string that is short enough");
//logger.info("strWidth: " + strWidth + " cell.width: " + cell.width + " idx: " + idx);
// Find previous whitespace run
while ( (idx > -1) && !Character.isWhitespace(text.charAt(idx)) ) {
idx--;
}
// Find last non-whatespace character before whitespace run.
while ( (idx > -1) && Character.isWhitespace(text.charAt(idx)) ) {
idx--;
}
if (idx < 1) {
break; // no spaces - have to put whole thing in cell and let it run over.
}
// Test new width
substr = text.substring(0, idx + 1);
strWidth = textStyle.stringWidthInDocUnits(substr);
}
wb.rows.add(WrappedRow.of(substr, strWidth, textStyle.lineHeight()));
// System.out.println("added row");
y -= textStyle.lineHeight();
// System.out.println("y=" + y);
// Chop off section of substring that we just wrote out.
text = substrNoLeadingWhitespace(text, substr.length());
if (strWidth > maxX) { maxX = strWidth; }
// System.out.println("maxX=" + maxX);
}
// // Not sure what to do if passed "". This used to mean to insert a blank line, but I'd
// // really like to make that "\n" instead, but don't have the time. *sigh*
// if (y == 0) {
// y -= textStyle.lineHeight();
// }
wb.blockDim = XyDim.of(maxX, 0 - y);
dims.put(maxWidth, wb);
// System.out.println("\tcalcWidth(" + maxWidth + ") on " + this.toString());
// System.out.println("\t\ttext calcDim() blockDim=" + wb.blockDim);
return wb.blockDim;
}
private WrappedBlock ensureWrappedBlock(final float maxWidth) {
WrappedBlock wb = dims.get(maxWidth);
if (wb == null) {
calcDimensionsForReal(maxWidth);
wb = dims.get(maxWidth);
}
return wb;
}
public XyDim calcDimensions(final float maxWidth) {
// I'd like to try to make calcDimensionsForReal() handle this situation before throwing an exception here.
// if (maxWidth < 0) {
// throw new IllegalArgumentException("maxWidth must be positive, not " + maxWidth);
// }
return ensureWrappedBlock(maxWidth).blockDim;
}
public XyOffset render(LogicalPage lp, XyOffset outerTopLeft, XyDim outerDimensions,
boolean allPages) {
// System.out.println("\tText.render(" + this.toString());
// System.out.println("\t\ttext.render(outerTopLeft=" + outerTopLeft +
// ", outerDimensions=" + outerDimensions);
float maxWidth = outerDimensions.x();
WrappedBlock wb = ensureWrappedBlock(maxWidth);
float x = outerTopLeft.x();
float y = outerTopLeft.y();
Padding innerPadding = align.calcPadding(outerDimensions, wb.blockDim);
// System.out.println("\t\ttext align.calcPadding() returns: " + innerPadding);
if (innerPadding != null) {
x += innerPadding.left();
//y -= innerPadding.top();
}
for (WrappedRow wr : wb.rows) {
// Here we're done whether it fits or not.
//final float xVal = x + align.leftOffset(wb.blockDim.x(), wr.rowDim.x());
y -= textStyle.ascent();
if (allPages) {
lp.borderStyledText(x, y, wr.string, textStyle);
} else {
lp.drawStyledText(x, y, wr.string, textStyle);
}
y -= textStyle.descent();
y -= textStyle.leading();
}
return XyOffset.of(outerTopLeft.x() + wb.blockDim.x(),
outerTopLeft.y() - wb.blockDim.y());
}
private static String substrNoLeadingWhitespace(final String text, int startIdx) {
// Drop any opening whitespace.
while ( (startIdx < text.length()) &&
Character.isWhitespace(text.charAt(startIdx))) {
startIdx++;
}
if (startIdx > 0) {
return text.substring(startIdx);
}
return text;
}
@Override
public String toString() {
return "Text(\"" + ((text.length() > 25) ? text.substring(0,22) + "..."
: text) +
"\")";
}
}