/*
* TV-Browser
* Copyright (C) 04-2003 Martin Oberhauser (martin_oat@yahoo.de)
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* CVS information:
* $RCSfile$
* $Source$
* $Date: 2010-08-07 21:06:58 +0200 (Sat, 07 Aug 2010) $
* $Author: bananeweizen $
* $Revision: 6694 $
*/
package util.misc;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.logging.Logger;
import net.davidashen.text.Hyphenator;
import net.davidashen.util.ErrorHandler;
import org.apache.commons.lang.StringUtils;
import tvbrowser.core.Settings;
import util.io.stream.InputStreamProcessor;
import util.io.stream.StreamUtilities;
/**
* Breaks a text into lines.
*
* @author Til Schneider, www.murfman.de
*/
public class TextLineBreakerStringWidth {
private static final String HYPHEN_DICT_FILENAME = "hyphen/dehyphx.tex";
private static final Logger mLog
= Logger.getLogger(TextLineBreakerStringWidth.class.getName());
/**
* ellipsis used for shortened titles and descriptions<br>
* unicode character representing "..."
*/
public static final String ELLIPSIS = "\u2026";
/** Current Character */
private int mCurrChar;
/** Line Buffer */
private StringBuilder mCurrLineBuffer;
/** Word Buffer */
private StringBuilder mCurrWordBuffer;
/** Next Word */
private String mNextWord;
/** Width of next Word */
private int mNextWordWidth;
/** Width of a Space-Character */
private int mSpaceWidth;
/** Width of a Minus-Character */
private int mMinusWidth;
private static Hyphenator hyphenator;
/**
* don't use hyphenator if it can not be initialized correctly
*/
private static boolean useHyphenator = false;
/**
* Create the LineBreaker
*/
public TextLineBreakerStringWidth() {
mCurrLineBuffer = new StringBuilder();
mCurrWordBuffer = new StringBuilder();
mSpaceWidth = 1;
mMinusWidth = 1;
if (Settings.propProgramPanelHyphenation.getBoolean()) {
initializeHyphenator();
}
}
private void initializeHyphenator() {
if (hyphenator != null) {
return;
}
hyphenator=new Hyphenator();
hyphenator.setErrorHandler(new ErrorHandler() {
@Override
public void debug(String arg0, String arg1) {
}
@Override
public void error(String arg0) {
mLog.severe(arg0);
}
@Override
public void exception(String arg0, Exception arg1) {
mLog.severe(arg0);
}
@Override
public void info(String arg0) {
mLog.info(arg0);
}
@Override
public boolean isDebugged(String arg0) {
return false;
}
@Override
public void warning(String arg0) {
mLog.warning(arg0);
}});
try {
File dictionary = new File(HYPHEN_DICT_FILENAME);
if (dictionary.exists()) {
StreamUtilities.inputStream(HYPHEN_DICT_FILENAME, new InputStreamProcessor() {
@Override
public void process(InputStream input) throws IOException {
hyphenator.loadTable(input);
useHyphenator = true;
}
});
}
else {
mLog.warning("Hyphenation dictionary not found at " + HYPHEN_DICT_FILENAME);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Set the Width of a Space Character
* @param spaceWidth new Space-Width
*/
public void setSpaceWidth(int spaceWidth) {
mSpaceWidth = spaceWidth;
}
/**
* Set the Width of a Minus Character
* @param minusWidth new Minus-Width
*/
public void setMinusWidth(int minusWidth) {
mMinusWidth = minusWidth;
}
/**
* Break a Text into separate Lines
* @param textReader Text to separate
* @param width Max-Width of each Line
* @return Text split in separate Lines
* @throws IOException
*/
public String[] breakLines(Reader textReader, int width) throws IOException {
return breakLines(textReader, width, Integer.MAX_VALUE);
}
/**
* Break a Text into separate Lines
* @param textReader Text to separate
* @param width Max-Width of each Line
* @param maxLines Max. amount of Lines
* @return Text split in separate Lines
* @throws IOException
*/
public String[] breakLines(Reader textReader, int width,
int maxLines)
throws IOException
{
if (width <= 0) {
width = Settings.propColumnWidth.getInt();
}
mNextWordWidth = -1;
if (maxLines == -1) {
maxLines = Integer.MAX_VALUE;
}
ArrayList<String> lineList = new ArrayList<String>();
boolean allProcessed;
do {
String line = readNextLine(textReader, width);
allProcessed = (mCurrChar == -1) && (mNextWordWidth == -1);
if (((lineList.size() + 1) == maxLines) && (! allProcessed)
&& (line.length() != 0))
{
// Add three dots if we stop because of the maxLines rule
line += ELLIPSIS;
}
lineList.add(line);
}
while ((lineList.size() < maxLines) && (! allProcessed));
int lastInx = lineList.size()-1;
String lastLine = lineList.get(lastInx);
if (StringUtils.isBlank(lastLine) || lineList.size() > maxLines) {
lineList.remove(lastInx);
}
String[] lineArr = new String[lineList.size()];
lineList.toArray(lineArr);
return lineArr;
}
/**
* Read the Next Line in TextReader
* @param textReader get next Line from this Reader
* @param maxWidth Max width of each Line
* @return one Line
* @throws IOException
*/
private String readNextLine(Reader textReader, int maxWidth)
throws IOException
{
// Clear the current line
mCurrLineBuffer.setLength(0);
int lineWidth = 0;
while (true) {
// Check whether there is a word that has to be processed first
if (mNextWordWidth == -1) {
// There is no unprocessed word any more -> Read to the next word
// (A length of -1 means it was processed)
// Ignore white space
do {
mCurrChar = textReader.read();
// Check whether we have to force a line break
if (isEndOfLine(mCurrChar)) {
// Force line break
return mCurrLineBuffer.toString();
}
}
while(Character.isSpaceChar(((char) mCurrChar)));
// Read the next word
mNextWord = readNextWord(textReader);
mNextWordWidth = getStringWidth(mNextWord);
}
int newLineWidth = lineWidth + mNextWordWidth;
if (lineWidth != 0) {
newLineWidth += mSpaceWidth;
}
int lineLength = mCurrLineBuffer.length();
if (newLineWidth - mSpaceWidth > maxWidth) {
// The next word does not fit
if (lineWidth == 0 || (maxWidth - lineWidth > 20)) {
// The line is empty -> Break the word
int breakPos = findBreakPos(mNextWord, maxWidth - lineWidth, lineWidth == 0);
if (breakPos <= 0) {
if (mCurrLineBuffer.length() > 0) { // avoid returning empty lines, leading to endless loops
return mCurrLineBuffer.toString();
}
else {
breakPos = Math.min(2, mNextWordWidth);
}
}
String firstPart = mNextWord.substring(0, breakPos);
if (lineLength > 0 && (mCurrLineBuffer.charAt(lineLength - 1) != '-' || (lineLength > 1 && mCurrLineBuffer.charAt(lineLength - 2) == ' '))) {
mCurrLineBuffer.append(' ');
}
mCurrLineBuffer.append(firstPart);
// Append a minus if the last character is a letter or digit
char lastChar = firstPart.charAt(firstPart.length() - 1);
if (Character.isLetterOrDigit(lastChar)) {
mCurrLineBuffer.append('-');
}
mNextWord = mNextWord.substring(breakPos);
mNextWordWidth = getStringWidth(mNextWord);
return mCurrLineBuffer.toString();
} else {
// Make a line break here (and process the word the next time)
return mCurrLineBuffer.toString();
}
} else {
if (lineWidth != 0) {
// Add a space, but not if our current word ends with "-"
char lastChar = mCurrLineBuffer.charAt(lineLength - 1);
if (lastChar != '/' && (lastChar != '-' || (lineLength >= 2 && mCurrLineBuffer.charAt(lineLength - 2) == ' '))) {
mCurrLineBuffer.append(' ');
}
lineWidth += mSpaceWidth;
}
// The next word fits -> Add it
mCurrLineBuffer.append(mNextWord);
lineWidth += mNextWordWidth;
mNextWordWidth = -1; // Mark the word as processed
// Check whether we have to force a line break
if (isEndOfLine(mCurrChar)) {
// Force line break
return mCurrLineBuffer.toString();
}
}
}
}
/**
* Read the next Word in TextReader
* @param textReader Get next Word from this TextReader
* @return next Word
* @throws IOException
*/
private String readNextWord(Reader textReader)
throws IOException
{
// Clear the current word
mCurrWordBuffer.setLength(0);
do {
mCurrWordBuffer.append((char) mCurrChar);
mCurrChar = textReader.read();
}
// a word stops at whitespace, line end or if a "-" occurs (but not if a space is in front of the "-")
while ((! Character.isWhitespace((char) mCurrChar)) && (! isEndOfLine(mCurrChar)) && (mCurrChar != '/') && (mCurrChar != '-' || mCurrWordBuffer.length() < 2));
if (mCurrChar == '/' || mCurrChar == '-') {
mCurrWordBuffer.append((char) mCurrChar);
}
return mCurrWordBuffer.toString();
}
/**
* Finds the best position to break the word in order to fit into a maximum
* width.
*
* @param word The word to break
* @param maxWidth The maximum width of the word
* @param mustBreak this word must break, even if no hyphenation is found
* @return The position where to break the word
*/
private int findBreakPos(final String word, int maxWidth, boolean mustBreak) {
// Reserve some space for the minus
maxWidth -= mMinusWidth;
// Binary search for the last fitting character
int left = 0;
int right = word.length() - 1;
while (left < right) {
int middle = (left + right + 1) / 2; // +1 to enforce taking the ceiling
// Check whether this substring fits
String subWord = word.substring(0, middle);
int subWordWidth = getStringWidth(subWord);
if (subWordWidth < maxWidth) {
// It fits -> go on with the right side
left = middle;
} else {
// It fits not -> go on with the left side
right = middle - 1;
}
}
int lastFittingPos = left;
// Try to find a char that is no letter or digit
// E.g. if the word is "Stadt-Land-Fluss" we try to break it in
// "Stadt-" and "Land-Fluss" rather than "Stadt-La" and "nd-Fluss"
for (int i = lastFittingPos - 1; i >= (lastFittingPos / 2); i--) {
char ch = word.charAt(i);
if (! Character.isLetterOrDigit(ch)) {
// This char is no letter or digit -> break here
return i + 1;
}
}
if (useHyphenator) {
int endCharacters;
if (Character.isLetter(word.charAt(word.length() - 1))) {
endCharacters = 2;
}
else {
endCharacters = 3; // some words end in punctuation, so make sure at least 2 letters stay together
}
int startCharacters = 2;
if (word.length() >= startCharacters + endCharacters) {
final String hyphenated = hyphenator.hyphenate(word, endCharacters, startCharacters);
if (hyphenated != null && hyphenated.length() > word.length()) {
int characters = 0;
int lastHyphen = 0;
for (int i = 0; i < hyphenated.length(); i++) {
if (hyphenated.charAt(i) != '\u00AD') {
if (++characters > lastFittingPos) {
return lastHyphen;
}
}
else {
lastHyphen = characters;
}
}
}
}
}
// We did not find a better break char -> break at the last fitting char
if (mustBreak) {
return lastFittingPos;
}
return 0;
}
/**
* Get the Width of a String
* @param str get Width of this String
* @return Width of this String
*/
public int getStringWidth(final String str) {
return str.length();
}
/**
* Test if the character is a EOL-Char
* @param ch test this Char
* @return true if ch is a EOL Char
*/
private boolean isEndOfLine(final int ch) {
return (ch == '\n') || (ch == -1);
}
/**
* to be used by Settings.handleChangedSettings()
*/
public static void resetHyphenator() {
hyphenator = null;
useHyphenator = false;
}
}