/*
Copyright (C) 2001, 2006, 2007 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.render;
import com.sun.opengl.util.j2d.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import java.util.regex.*;
/**
* Multi line, rectangle bound text renderer with (very) minimal html support.
*<p>
* The {@link MultiLineTextRenderer} (MLTR) handles wrapping, measuring and drawing
* of multiline text strings using Sun's JOGL {@link TextRenderer}.
*</p>
* <p>
* A multiline text string is a character string containing new line characters
* in between lines.
* </p>
* <p>
* MLTR can handle both regular text with new line seprators and a very minimal
* implementation of HTML. Each type of text has its own methods though.
*</p>
*
* <p><b>Usage:</b></p>
*
* <p>Instantiation:</p>
* <p>
* The MLTR needs a Font or a TextRenderer to be instanciated. This will be
* the font used for text drawing, wrapping and measuring. For HTML methods
* this font will be considered as the document default font.
* </p>
* <pre>
* Font font = Font.decode("Arial-PLAIN-12");
* MultiLineTextRenderer mltr = new MultiLineTextRenderer(font);
* </pre>
* or
* <pre>
* TextRenderer tr = new TextRenderer(Font.decode("Arial-PLAIN-10"));
* MultiLineTextRenderer mltr = new MultiLineTextRenderer(tr);
* </pre>
*
* <p>Drawing regular text:</p>
* <pre>
* String text = "Line one.\nLine two.\nLine three...";
* int x = 10; // Upper left corner of text rectangle.
* int y = 200; // Origin at bottom left of screen.
* int lineHeight = 14; // Line height in pixels.
* Color color = Color.RED;
*
* mltr.setTextColor(color);
* mltr.getTextRenderer().begin3DRendering();
* mltr.draw(text, x, y, lineHeight);
* mltr.getTextRenderer().end3DRendering();
* </pre>
*
* <p>Wrapping text to fit inside a width and optionaly a height</p>
* <p>
* The MLTR wrap method will insert new line characters inside the text so that
* it fits a given width in pixels.
* </p>
* <p>
* If a height dimension above zero is specified too, the text will be truncated
* if needed, and a continuation string will be appended to the last line. The
* continuation string can be set with mltr.setContinuationString();
* </p>
* <pre>
* // Fit inside 300 pixels, no height constraint
* String wrappedText = mltr.wrap(text, new Dimension(300, 0));
*
* // Fit inside 300x400 pixels, text may be truncated
* String wrappedText = mltr.wrap(text, new Dimension(300, 400));
* </pre>
*
* <p>Measuring text</p>
* <pre>
* Rectangle2D textBounds = mltr.getBounds(text);
* </pre>
* <p>
* The textBounds rectangle returned contains the width and height of the text
* as it would be drawn with the current font.
* </p>
* <p>
* Note that textBounds.minX is the number of lines found and textBounds.minY
* is the maximum line height for the font used. This value can be safely used
* as the lineHeight argument when drawing - or can even be ommited after a
* getBounds: draw(text, x, y);
* ...
* </p>
*
* <p><b>HTML support</b></p>
* <p>
* Supported tags are:
* <ul>
* <li><p></p>, <br> <br /></li>
* <li><b></b></li>
* <li><i></i></li>
* <li><a href="..."></a></li>
* <li><font color="#ffffff"></font></li>
* </ul>
* </p>
* ...
*
*
*
* <p>
* See {@link AbstractAnnotation}.drawAnnotation() for more usage details.
* </p>
*
* @author: Patrick Murris
* @version $Id: MultiLineTextRenderer.java 5028 2008-04-11 19:50:38Z tgaskins $
*/
public class MultiLineTextRenderer
{
public final static int ALIGN_LEFT = 0;
public final static int ALIGN_CENTER = 1;
public final static int ALIGN_RIGHT = 2;
public static final String EFFECT_NONE = "render.MultiLineTextRenderer.EffectNone";
public static final String EFFECT_SHADOW = "render.MultiLineTextRenderer.EffectShadow";
public static final String EFFECT_OUTLINE = "render.MultiLineTextRenderer.EffectOutline";
private TextRenderer textRenderer;
private int lineSpacing = 0; // Inter line spacing in pixels
private int lineHeight = 14; // Will be set by getBounds() or by application
private int textAlign = ALIGN_LEFT; // Text alignement
private String continuationString = "...";
private Color textColor = Color.DARK_GRAY;
private Color backColor = Color.LIGHT_GRAY;
private Color linkColor = Color.BLUE;
public MultiLineTextRenderer(TextRenderer textRenderer)
{
if(textRenderer == null)
{
String msg = Logging.getMessage("nullValue.TextRendererIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.textRenderer = textRenderer;
}
public MultiLineTextRenderer(Font font)
{
if(font == null)
{
String msg = Logging.getMessage("nullValue.FontIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.textRenderer = new TextRenderer(font, true, true);
this.textRenderer.setUseVertexArrays(false);
}
/**
* Get the current TextRenderer.
* @return the current TextRenderer.
*/
public TextRenderer getTextRenderer()
{
return this.textRenderer;
}
/**
* Get the current line spacing height in pixels.
* @return the current line spacing height in pixels.
*/
public int getLineSpacing()
{
return this.lineSpacing;
}
/**
* Set the current line spacing height in pixels.
* @param height the line spacing height in pixels.
*/
public void setLineSpacing(int height)
{
this.lineSpacing = height;
}
/**
* Get the current line height in pixels.
* @return the current line height in pixels.
*/
public int getLineHeight()
{
return this.lineHeight;
}
/**
* Set the current line height in pixels.
* @param height the current line height in pixels.
*/
public void setLineHeight(int height)
{
this.lineHeight = height;
}
/**
* Get the current text alignment. Can be one of {@link #ALIGN_LEFT} the default,
* {@link #ALIGN_CENTER} or {@link #ALIGN_RIGHT}.
* @return the current text alignment.
*/
public int getTextAlign()
{
return this.textAlign;
}
/**
* Set the current text alignment. Can be one of {@link #ALIGN_LEFT} the default,
* {@link #ALIGN_CENTER} or {@link #ALIGN_RIGHT}.
* @param align the current text alignment.
*/
public void setTextAlign(int align)
{
if(align != ALIGN_LEFT && align != ALIGN_CENTER && align != ALIGN_RIGHT)
{
String msg = Logging.getMessage("generic.ArgumentOutOfRange", align);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.textAlign = align;
}
/**
* Get the current text color.
* @return the current text color.
*/
public Color getTextColor()
{
return this.textColor;
}
/**
* Set the text renderer color.
* @param color the color to use when drawing text.
*/
public void setTextColor(Color color)
{
if(color == null)
{
String msg = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.textColor = color;
this.textRenderer.setColor(color);
}
/**
* Get the background color used for EFFECT_SHADOW and EFFECT_OUTLINE.
* @return the current background color used when drawing shadow or outline..
*/
public Color getBackColor()
{
return this.backColor;
}
/**
* Set the background color used for EFFECT_SHADOW and EFFECT_OUTLINE.
* @param color the color to use when drawing shadow or outline.
*/
public void setBackColor(Color color)
{
if(color == null)
{
String msg = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.backColor = color;
}
/**
* Get the current link color.
* @return the current link color.
*/
public Color getLinkColor()
{
return this.linkColor;
}
/**
* Set the link color.
* @param color the color to use when drawing hyperlinks.
*/
public void setLinkColor(Color color)
{
if(color == null)
{
String msg = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.linkColor = color;
}
/**
* Set the character string appended at the end of text truncated during
* a wrap operation when exceeding the given height limit.
* @param s the continuation character string.
*/
public void setContinuationString(String s)
{
this.continuationString = s;
}
/**
* Get the maximum line height for the given text renderer.
* @param tr the TextRenderer.
* @return the maximum line height.
*/
public double getMaxLineHeight(TextRenderer tr)
{
// Check underscore + capital E with acute accent
return tr.getBounds("_\u00c9").getHeight();
}
//** Plain text support ******************************************************
//****************************************************************************
/**
* Returns the bounding rectangle for a multi-line string.
* Note that the X component of the rectangle is the number of lines found in the text
* and the Y component of the rectangle is the max line height encountered.
* Note too that this method will automatically set the current line height to the max height found.
* @param text the multi-line text to evaluate.
* @return the bounding rectangle for the string.
*/
public Rectangle getBounds(String text)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
int width = 0;
int maxLineHeight = 0;
String[] lines = text.split("\n");
for(int i = 0; i < lines.length; i++)
{
Rectangle2D lineBounds = this.textRenderer.getBounds(lines[i]);
width = (int)Math.max(lineBounds.getWidth(), width);
maxLineHeight = (int)Math.max(lineBounds.getHeight(), lineHeight);
}
// Make sure we have the highest line height
maxLineHeight = (int)Math.max(getMaxLineHeight(this.textRenderer), maxLineHeight);
// Set current line height for future draw
this.lineHeight = maxLineHeight;
// Compute final height using maxLineHeight and number of lines
return new Rectangle(lines.length, lineHeight, width,
lines.length * maxLineHeight + (lines.length - 1) * this.lineSpacing);
}
/**
* Draw a multi-line text string with bounding rectangle top starting at the y position.
* Depending on the current textAlign, the x position is either the rectangle left side,
* middle or right side.
* Uses the current line height.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
*/
public void draw(String text, int x, int y)
{
this.draw(text, x, y, this.lineHeight);
}
/**
* Draw a multi-line text string with bounding rectangle top starting at the y position.
* Depending on the current textAlign, the x position is either the rectangle left side,
* middle or right side.
* Uses the current line height and the given effect.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
* @param effect the effect to use for the text rendering. Can be one of <code>EFFECT_NONE</code>,
* <code>EFFECT_SHADOW</code> or <code>EFFECT_OUTLINE</code>.
*/
public void draw(String text, int x, int y, String effect)
{
this.draw(text, x, y, this.lineHeight, effect);
}
/**
* Draw a multi-line text string with bounding rectangle top starting at the y position.
* Depending on the current textAlign, the x position is either the rectangle left side,
* middle or right side.
* Uses the given line height and effect.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
* @param textLineHeight the line height in pixels.
* @param effect the effect to use for the text rendering. Can be one of <code>EFFECT_NONE</code>,
* <code>EFFECT_SHADOW</code> or <code>EFFECT_OUTLINE</code>.
*/
public void draw(String text, int x, int y, int textLineHeight, String effect)
{
if(effect == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (effect.equals(EFFECT_SHADOW))
{
this.textRenderer.setColor(backColor);
this.draw(text, x + 1, y - 1, textLineHeight);
this.textRenderer.setColor(textColor);
}
else if (effect.equals(EFFECT_OUTLINE))
{
this.textRenderer.setColor(backColor);
this.draw(text, x, y + 1, textLineHeight);
this.draw(text, x + 1, y, textLineHeight);
this.draw(text, x, y - 1, textLineHeight);
this.draw(text, x - 1, y, textLineHeight);
this.textRenderer.setColor(textColor);
}
// Draw normal text
this.draw(text, x, y, textLineHeight);
}
/**
* Draw a multi-line text string with bounding rectangle top starting at the y position.
* Depending on the current textAlign, the x position is either the rectangle left side,
* middle or right side.
* Uses the given line height.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
* @param textLineHeight the line height in pixels.
*/
public void draw(String text, int x, int y, int textLineHeight)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
String[] lines = text.split("\n");
for(int i = 0; i < lines.length; i++)
{
int xAligned = x;
if(this.textAlign == ALIGN_CENTER)
xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth() / 2);
else if(this.textAlign == ALIGN_RIGHT)
xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth());
y -= textLineHeight;
this.textRenderer.draw3D(lines[i], xAligned, y, 0, 1);
y -= this.lineSpacing;
}
}
/**
* Draw text with unique colors word bounding rectangles and add each as a pickable object
* to the provided PickSupport instance.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
* @param textLineHeight the line height in pixels.
* @param dc the current DrawContext.
* @param pickSupport the PickSupport instance to be used.
* @param refObject the user reference object associated with every picked word.
* @param refPosition the user reference Position associated with every picked word.
*/
public void pick(String text, int x, int y, int textLineHeight,
DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(pickSupport == null)
{
String msg = Logging.getMessage("nullValue.PickSupportIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
String[] lines = text.split("\n");
for(int i = 0; i < lines.length; i++)
{
int xAligned = x;
if(this.textAlign == ALIGN_CENTER)
xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth() / 2);
else if(this.textAlign == ALIGN_RIGHT)
xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth());
y -= textLineHeight;
drawLineWithUniqueColors(lines[i], xAligned, y, dc, pickSupport, refObject, refPosition);
y -= this.lineSpacing;
}
}
private void drawLineWithUniqueColors(String text, int x, int y,
DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
{
float spaceWidth = this.textRenderer.getCharWidth(' ');
float drawX = x;
float drawY = y;
String source = text.trim();
int start = 0;
int end = source.indexOf(' ', start + 1);
while(start < source.length())
{
if(end == -1)
end = source.length(); // last word
// Extract a 'word' which is in fact a space and a word except for first word
String word = source.substring(start, end);
// Measure word and already draw line part - from line beginning
Rectangle2D wordBounds = this.textRenderer.getBounds(word);
Rectangle2D drawnBounds = this.textRenderer.getBounds(source.substring(0, start));
float space = word.charAt(0) == ' ' ? spaceWidth : 0f;
drawX = x + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
// Add pickable object
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
PickedObject po = new PickedObject(colorCode, refObject, refPosition, false);
po.setValue(AVKey.TEXT, word.trim());
pickSupport.addPickableObject(po);
// Draw word rectangle
dc.getGL().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
drawFilledRectangle(dc, drawX + wordBounds.getX(), drawY - wordBounds.getHeight() - wordBounds.getY(),
wordBounds.getWidth(), wordBounds.getHeight());
// Move forward in source string
start = end;
if(start < source.length() - 1)
{
end = source.indexOf(' ', start + 1);
}
}
}
/**
* Add 'new line' characters inside a string so that it's bounding rectangle
* tries not to exceed the given dimension width.
* If the dimension height is more than zero, the text will be truncated accordingly and
* the continuation string will be appended to the last line.
* Note that words will not be split and at least one word will be used per line
* so the longest word defines the final width of the bounding rectangle.
* Each line is trimmed of leading and trailing spaces.
* @param text the text string to wrap
* @param dimension the maximum dimension in pixels
* @return the wrapped string
*/
public String wrap(String text, Dimension dimension)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(dimension == null)
{
String msg = Logging.getMessage("nullValue.DimensionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
int width = (int)dimension.getWidth();
int height = (int)dimension.getHeight();
String[] lines = text.split("\n");
StringBuffer wrappedText = new StringBuffer();
// Wrap each line
for(int i = 0; i < lines.length; i++)
{
lines[i] = this.wrapLine(lines[i], width);
}
// Concatenate all lines in one string with new line separators
// between lines - not at the end
// Checks for height limit.
int currentHeight = 0;
boolean heightExceeded = false;
double maxLineHeight = getMaxLineHeight(this.textRenderer);
for(int i = 0; i < lines.length && !heightExceeded; i++)
{
String[] subLines = lines[i].split("\n");
for(int j = 0; j < subLines.length && !heightExceeded; j++)
{
if(height <= 0 || currentHeight + maxLineHeight <= height)
{
wrappedText.append(subLines[j]);
currentHeight += maxLineHeight + this.lineSpacing;
if(j < subLines.length - 1)
wrappedText.append('\n');
}
else
{
heightExceeded = true;
}
}
if(i < lines.length - 1 && !heightExceeded)
wrappedText.append('\n');
}
// Add continuation string if text truncated
if(heightExceeded)
{
if(wrappedText.length() > 0)
wrappedText.deleteCharAt(wrappedText.length() - 1); // Remove excess new line
wrappedText.append(this.continuationString);
}
return wrappedText.toString();
}
// Wrap one line to fit the given width
private String wrapLine(String text, int width)
{
StringBuffer wrappedText = new StringBuffer();
// Single line - trim leading and trailing spaces
String source = text.trim();
Rectangle2D lineBounds = this.textRenderer.getBounds(source);
if(lineBounds.getWidth() > width)
{
// Split single line to fit preferred width
StringBuffer line = new StringBuffer();
int start = 0;
int end = source.indexOf(' ', start + 1);
while(start < source.length())
{
if(end == -1)
end = source.length(); // last word
// Extract a 'word' which is in fact a space and a word
String word = source.substring(start, end);
String linePlusWord = line + word;
if(this.textRenderer.getBounds(linePlusWord).getWidth() <= width)
{
// Keep adding to the current line
line.append(word);
}
else
{
// Width exceeded
if(line.length() != 0 )
{
// Finish current line and start new one
wrappedText.append(line);
wrappedText.append('\n');
line.delete(0, line.length());
line.append(word.trim()); // get read of leading space(s)
}
else
{
// Line is empty, force at least one word
line.append(word.trim());
}
}
// Move forward in source string
start = end;
if(start < source.length() - 1)
{
end = source.indexOf(' ', start + 1);
}
}
// Gather last line
wrappedText.append(line);
}
else
{
// Line doesnt need to be wrapped
wrappedText.append(source);
}
return wrappedText.toString();
}
//** Very very simple html support *******************************************
// Handles <P></P>, <BR /> or <BR>, <B></B>, <I></I>, <A HREF="..."></A>
// and <font color="#ffffff"></font>.
//****************************************************************************
/**
* Return true if the text contains some sgml tags.
* @param text The text string to evaluate.
* @return true if the string contains sgml or html tags
*/
public static boolean containsHTML(String text)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
Pattern pattern = Pattern.compile("<[^\\s].*?>"); // Match any sgml tag
Matcher matcher = pattern.matcher(text);
return matcher.find();
}
/**
* Remove new line characters then replace BR and P tags with appropriate new lines
* @param text The html text string to process.
* @return The processed text string.
*/
public static String processLineBreaksHTML(String text)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
text = text.replaceAll("\n", ""); // Remove all new line characters
text = text.replaceAll("(?i)<br\\s?.*?>", "\n"); // Replace <br ...> with one new line
text = text.replaceAll("(?i)<p\\s?.*?>", ""); // Replace <p ...> with nothing
text = text.replaceAll("(?i)</p>", "\n\n"); // Replace </p> with two new line
return text;
}
/**
* Remove all HTML tags from a text string.
* @param text the string to filter.
* @return the filtered string.
*/
public static String removeTagsHTML(String text)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
return text.replaceAll("<[^\\s].*?>", "");
}
/**
* Extract an attribute value from a HTML tag string. The attribute is expected to be formed
* on the pattern: name="...". Other variants will likely fail.
* @param text the HTML tage string.
* @param attributeName the attribute name.
* @return the attribute value found. Null if empty or not found.
*/
public static String getAttributeFromTagHTML(String text, String attributeName)
{
if(text == null || attributeName == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
// Look for name="..." - will not work for other variants
Pattern pattern = Pattern.compile("(?i)" + attributeName.toLowerCase() + "=\"([^\"].*?)\"");
Matcher matcher = pattern.matcher(text);
if (matcher.find())
return matcher.group(1);
return null;
}
/**
* Returns the bounding rectangle for a multi-line html string.
* Note that the X component of the rectangle is the number of lines found in the text
* and the Y component of the rectangle is the average line height encountered.
* @param text the multi-line html text to evaluate.
* @param renderers A HashMap of fonts and shared text renderers.
* @return the bounding rectangle for the rendered text.
*/
public Rectangle2D getBoundsHTML(String text, TextRendererCache renderers)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(renderers == null)
{
String msg = Logging.getMessage("nullValue.TextRendererCacheIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
return getBoundsHTML(text, ds);
}
/**
* Returns the bounding rectangle for a multi-line html string.
* Note that the X component of the rectangle is the number of lines found in the text
* and the Y component of the rectangle is the average line height encountered.
* @param text the multi-line html text to evaluate.
* @param dsCurrent The current DrawState.
* @return the bounding rectangle for the rendered text.
*/
private Rectangle2D getBoundsHTML(String text, DrawState dsCurrent)
{
String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
Pattern pattern = Pattern.compile(regex);
// Use a copy of DrawState - do not alter original
DrawState ds = new DrawState(dsCurrent);
// Spilt string
double width = 0;
double height = 0;
String[] lines = text.split("\n");
StringBuffer linePart = new StringBuffer();
for(int i = 0; i < lines.length; i++)
{
// Measure each line
int start = 0;
double lineWidth = 0;
double maxLineHeight = getMaxLineHeight(ds.textRenderer);
linePart.delete(0, linePart.length());
Matcher matcher = pattern.matcher(lines[i]);
while (matcher.find()) {
if(matcher.group().equals(" "))
{
// Space found, concatenate and keep going
linePart.append(lines[i].substring(start, matcher.start()));
start = matcher.start(); // move on
}
else
{
// Html tag found
// Process current line part and measure - use counterTrim() workaround
linePart.append(lines[i].substring(start, matcher.start()));
if(linePart.length() > 0)
{
Rectangle2D partBounds = ds.textRenderer.getBounds(counterTrim(linePart));
//Rectangle2D partBounds = currentTextRenderer.getBounds(linePart);
lineWidth += partBounds.getWidth() + partBounds.getX();
linePart.delete(0, linePart.length()); // clear part
}
start = matcher.end(); // move on
// Process html tag and update draw attributes
ds.update(matcher.group(), false);
// Keep track of max line height
maxLineHeight = (int)Math.max(getMaxLineHeight(ds.textRenderer), maxLineHeight);
}
}
// Gather and measure end of line
if(start < lines[i].length())
{
linePart.append(lines[i].substring(start));
if(linePart.length() > 0)
{
//Rectangle2D partBounds = currentTextRenderer.getBounds(counterTrim(linePart));
Rectangle2D partBounds = ds.textRenderer.getBounds(linePart);
lineWidth += partBounds.getWidth() + partBounds.getX();
maxLineHeight = (int)Math.max(partBounds.getHeight(), maxLineHeight);
}
}
// Accumulate dimensions
width = Math.max(width, lineWidth);
height += maxLineHeight + this.lineSpacing;
}
height -= this.lineSpacing; // subtract last line spacing
// Return bounds - Note that minX is the number of lines and minY is the line height average
return new Rectangle(lines.length, (int)(height / lines.length),
(int)Math.round(width), (int)Math.round(height));
}
/**
* Draw a multi-line html text string with bounding rectangle top starting at the y position. The x
* position is eiher the rectangle left side, middle or right side depending on the current text alignement.
* @param text the multi-line text to draw
* @param x the x position for top left corner of text rectangle
* @param y the y position for top left corner of the text rectangle
* @param renderers A HashMap of fonts and shared text renderers.
*/
public void drawHTML(String text, int x, int y, TextRendererCache renderers)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(renderers == null)
{
String msg = Logging.getMessage("nullValue.TextRendererCacheIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
Pattern pattern = Pattern.compile(regex);
// Draw attributes
DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
// Draw string
int baseX = x;
double drawY = y;
ds.textRenderer.begin3DRendering();
ds.textRenderer.setColor(this.textColor);
String[] lines = text.split("\n");
StringBuffer linePart = new StringBuffer();
for(int i = 0; i < lines.length; i++)
{
// Set line start x
double drawX = baseX;
Rectangle2D lineBounds = getBoundsHTML(lines[i], ds);
if(this.textAlign == ALIGN_CENTER)
drawX = x - (int)(lineBounds.getWidth() / 2);
else if(this.textAlign == ALIGN_RIGHT)
drawX = x - (int)(lineBounds.getWidth());
// Skip line height
drawY -= lineBounds.getHeight();
// Draw one line
int start = 0;
linePart.delete(0, linePart.length());
Matcher matcher = pattern.matcher(lines[i]);
while (matcher.find()) {
if(matcher.group().equals(" "))
{
// Space found, concatenate and keep going
linePart.append(lines[i].substring(start, matcher.start()));
start = matcher.start(); // move on
}
else
{
// Html tag found
// Process current line part and draw
linePart.append(lines[i].substring(start, matcher.start()));
if(linePart.length() > 0)
{
// Draw
ds.textRenderer.draw3D(linePart, (int)Math.round(drawX), (int)Math.round(drawY), 0, 1);
// Move x - use antiTrim() workaround
Rectangle2D partBounds = ds.textRenderer.getBounds(counterTrim(linePart));
//Rectangle2D partBounds = currentTextRenderer.getBounds(linePart);
drawX += partBounds.getWidth() + partBounds.getX();
linePart.delete(0, linePart.length()); // clear part
}
start = matcher.end(); // move on
// Process html tag and update draw attributes
ds.update(matcher.group(), true);
}
}
// Gather and draw end of line
if(start < lines[i].length())
{
linePart.append(lines[i].substring(start));
if(linePart.length() > 0)
ds.textRenderer.draw3D(linePart, (int)Math.round(drawX), (int)Math.round(drawY), 0, 1);
}
// Skip line spacing
drawY -= this.lineSpacing;
}
ds.textRenderer.end3DRendering();
}
/**
* Draw text with unique colors word bounding rectangles and add each as a pickable object
* to the provided PickSupport instance.
* @param text the multi-line text to draw.
* @param x the x position for top left corner of text rectangle.
* @param y the y position for top left corner of the text rectangle.
* @param renderers A HashMap of fonts and shared text renderers.
* @param dc the current DrawContext.
* @param pickSupport the PickSupport instance to be used.
* @param refObject the user reference object associated with every picked word.
* @param refPosition the user reference Position associated with every picked word.
*/
public void pickHTML(String text, int x, int y, TextRendererCache renderers,
DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(renderers == null)
{
String msg = Logging.getMessage("nullValue.TextRendererCacheIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(pickSupport == null)
{
String msg = Logging.getMessage("nullValue.PickSupportIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
Pattern pattern = Pattern.compile(regex);
// Draw attributes
DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
// Draw string
double drawX = x;
double drawY = y;
String[] lines = text.split("\n");
StringBuffer linePart = new StringBuffer();
for(int i = 0; i < lines.length; i++)
{
// Set line start x
double baseX = x;
Rectangle2D lineBounds = getBoundsHTML(lines[i], ds);
if(this.textAlign == ALIGN_CENTER)
baseX = x - (int)(lineBounds.getWidth() / 2);
else if(this.textAlign == ALIGN_RIGHT)
baseX = x - (int)(lineBounds.getWidth());
// Skip line height
drawY -= lineBounds.getHeight();
// Save draw state at beginning of line and word
DrawState dsLine = new DrawState(ds);
DrawState dsWord = new DrawState(ds);
// Draw one line
int wordStart = -1;
int start = 0;
linePart.delete(0, linePart.length());
Matcher matcher = pattern.matcher(lines[i]);
while (matcher.find()) {
if(matcher.group().equals(" "))
{
// Space found - get and measure new word and already drawn part
String word = wordStart == -1 ? lines[i].substring(start, matcher.start())
: lines[i].substring(wordStart, matcher.start());
String drawn = wordStart == -1 ? lines[i].substring(0, start)
: lines[i].substring(0, wordStart);
Rectangle2D wordBounds = getBoundsHTML(word, dsWord);
Rectangle2D drawnBounds = getBoundsHTML(drawn, dsLine);
// get current hyperlink
String hyperlink = dsWord.getDrawAttributes().hyperlink;
// Draw word bounding rectangle
drawX = baseX + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
pickWord( word, hyperlink, drawX, drawY, wordBounds, dc, pickSupport, refObject, refPosition);
// Save draw state for next word
dsWord = new DrawState(ds);
start = matcher.start(); // move on from space found
wordStart = -1;
}
else
{
// Html tag found
wordStart = wordStart == -1 ? start : wordStart;
start = matcher.end(); // move on from after tag
// Process html tag and update draw attributes
ds.update(matcher.group(), false);
// Propagate hyperlink to current word draw state if not null
dsWord.getDrawAttributes().hyperlink = ds.getDrawAttributes().hyperlink != null ?
ds.getDrawAttributes().hyperlink : dsWord.getDrawAttributes().hyperlink;
}
}
// Gather and draw end of line
if(start < lines[i].length() || wordStart != -1)
{
String word = wordStart == -1 ? lines[i].substring(start) : lines[i].substring(wordStart);
String drawn = wordStart == -1 ? lines[i].substring(0, start) : lines[i].substring(0, wordStart);
Rectangle2D wordBounds = getBoundsHTML(word, dsWord);
Rectangle2D drawnBounds = getBoundsHTML(drawn, dsLine);
// get current hyperlink
String hyperlink = dsWord.getDrawAttributes().hyperlink;
// Draw word bounding rectangle
drawX = baseX + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
pickWord( word, hyperlink, drawX, drawY, wordBounds, dc, pickSupport, refObject, refPosition);
}
// Skip line spacing
drawY -= this.lineSpacing;
}
}
private void pickWord(String word, String hyperlink, double drawX, double drawY, Rectangle2D wordBounds,
DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
{
// Add pickable object
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
PickedObject po = new PickedObject(colorCode, refObject, refPosition, false);
po.setValue(AVKey.TEXT, removeTagsHTML(word.trim()));
if(hyperlink != null)
po.setValue(AVKey.URL, hyperlink);
pickSupport.addPickableObject(po);
// Draw word rectangle
dc.getGL().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
drawFilledRectangle(dc, drawX, drawY - wordBounds.getHeight() / 5,
wordBounds.getWidth(), wordBounds.getHeight());
}
/**
* Add 'new line' characters inside an html text string so that it's bounding rectangle
* tries not to exceed the given dimension width.
* If the dimension height is more than zero, the text will be truncated accordingly and
* the continuation string will be appended to the last line.
* Note that words will not be split and at least one word will be used per line
* so the longest word defines the final width of the bounding rectangle.
* Each line is trimmed of leading and trailing spaces.
* @param text the html text string to wrap
* @param dimension the maximum dimension in pixels
* @param renderers A HashMap of fonts and shared text renderers.
* @return the wrapped html string
*/
public String wrapHTML(String text, Dimension dimension, TextRendererCache renderers)
{
if(text == null)
{
String msg = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(dimension == null)
{
String msg = Logging.getMessage("nullValue.DimensionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if(renderers == null)
{
String msg = Logging.getMessage("nullValue.TextRendererCacheIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
Pattern pattern = Pattern.compile(regex);
// Draw attributes
DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
int width = (int)dimension.getWidth();
int height = (int)dimension.getHeight();
// Split string
String[] lines = text.split("\n");
StringBuffer wrappedText = new StringBuffer();
int currentHeight = 0;
int lineCount = 0;
boolean heightExceeded = false;
for(int i = 0; i < lines.length && !heightExceeded; i++)
{
// Single line - trim leading and trailing spaces
String source = lines[i].trim();
double maxLineHeight = getMaxLineHeight(ds.textRenderer);
Rectangle2D lineBounds = getBoundsHTML(source, ds);
if(lineBounds.getWidth() > width)
{
// Split single line to fit preferred width
StringBuffer line = new StringBuffer();
double lineWidth = 0;
double wordWidth = 0;
int wordStart = -1;
int start = 0;
Matcher matcher = pattern.matcher(source);
while (matcher.find() && !heightExceeded)
{
if(matcher.group().equals(" "))
{
// Space found - check new word length and line total
String word = source.substring(start, matcher.start());
Rectangle2D wordBounds = getBoundsHTML(word, ds);
wordWidth += wordBounds.getWidth() + wordBounds.getX();
// If word already started earlier, gather the full word
if(wordStart != -1)
word = source.substring(wordStart, matcher.start());
if(lineWidth + wordWidth <= width)
{
// Keep adding to the current line
line.append(word);
lineWidth += wordWidth;
}
else
{
// Width exceeded
word = word.trim(); // get read of leading space(s)
wordBounds = getBoundsHTML(word, ds);
wordWidth = wordBounds.getWidth() + wordBounds.getX();
if(line.length() != 0 )
{
// Finish current line and start new one
if(height <= 0 || currentHeight + maxLineHeight <= height)
{
wrappedText.append(line);
wrappedText.append('\n');
currentHeight += maxLineHeight + this.lineSpacing;
lineCount++;
line.delete(0, line.length());
line.append(word);
lineWidth = wordWidth;
// Keep track of max line height
maxLineHeight = getMaxLineHeight(ds.textRenderer);
}
else
{
heightExceeded = true;
}
}
else
{
// Line is empty, force at least one word
line.append(word);
lineWidth = wordWidth;
}
}
// Move on from space found
start = matcher.start();
wordWidth = 0;
wordStart = -1;
}
else
{
// Html tag found
// Process line part and measure - use counterTrim() workaround
// Accumulate wordWidth and set wordStart to decide latter whether this is going on the current line
if(matcher.start() > start)
{
String word = source.substring(start, matcher.start());
Rectangle2D wordBounds = getBoundsHTML(counterTrim(word), ds);
wordWidth += wordBounds.getWidth() + wordBounds.getX();
}
wordStart = wordStart == -1 ? start : wordStart;
start = matcher.end(); // move on
// Process html tag and update draw attributes
ds.update(matcher.group(), false);
// Keep track of max line height
maxLineHeight = (int)Math.max(getMaxLineHeight(ds.textRenderer), maxLineHeight);
}
}
// Gather and measure end of line if any
if((start < source.length() || wordStart != -1) && !heightExceeded)
{
String word = "";
if(start < source.length())
{
// Gather last bit and add to wordWidth
word = source.substring(start);
Rectangle2D wordBounds = getBoundsHTML(word, ds);
wordWidth += wordBounds.getWidth() + wordBounds.getX();
}
// If word already started earlier, gather the full word
if(wordStart != -1)
word = source.substring(wordStart);
if(lineWidth + wordWidth <= width)
{
// Keep adding to the current line
line.append(word);
}
else
{
// Width exceeded
word = word.trim(); // get read of leading space(s)
if(line.length() != 0 )
{
// Finish current line and start new one
if(height <= 0 || currentHeight + maxLineHeight <= height)
{
wrappedText.append(line);
wrappedText.append('\n');
currentHeight += maxLineHeight + this.lineSpacing;
lineCount++;
line.delete(0, line.length());
line.append(word);
// Keep track of max line height
maxLineHeight = getMaxLineHeight(ds.textRenderer);
}
else
{
heightExceeded = true;
}
}
else
{
// Line is empty, force at least one word
line.append(word);
}
}
if(height <= 0 || currentHeight + maxLineHeight <= height)
{
wrappedText.append(line);
currentHeight += maxLineHeight + this.lineSpacing;
lineCount++;
}
else
{
heightExceeded = true;
}
}
}
else
{
// line doesnt need to be wrapped
if(height <= 0 || currentHeight + maxLineHeight <= height)
{
wrappedText.append(source);
currentHeight += maxLineHeight + this.lineSpacing;
lineCount++;
}
else
{
heightExceeded = true;
}
}
// Add new line between lines - not after the last one.
if(i < lines.length - 1 && !heightExceeded)
wrappedText.append('\n');
}
// Add continuation string if text truncated
if(heightExceeded)
{
if(wrappedText.length() > 0)
wrappedText.deleteCharAt(wrappedText.length() - 1); // Remove excess new line
wrappedText.append(this.continuationString);
}
return wrappedText.toString();
}
// Replace first leading space and last trailing space with the character 't'.
// This is a workaround for TextRenderer.getBounds() which ignores leading and trailing spaces.
private String counterTrim(StringBuffer s)
{
if(s.length() == 0)
return "";
StringBuffer sbOut = new StringBuffer(s);
if(sbOut.substring(sbOut.length() - 1).equals(" "))
sbOut.setCharAt(s.length() - 1, 't'); // use a 't' to fillup last space
if(sbOut.substring(0, 1).equals(" "))
sbOut.setCharAt(0, 't'); // use a 't' to fillup leading space
return sbOut.toString();
}
private String counterTrim(String s)
{
if(s.length() == 0)
return "";
StringBuffer sbOut = new StringBuffer(s);
if(sbOut.substring(sbOut.length() - 1).equals(" "))
sbOut.setCharAt(s.length() - 1, 't'); // use a 't' to fillup last space
if(sbOut.substring(0, 1).equals(" "))
sbOut.setCharAt(0, 't'); // use a 't' to fillup leading space
return sbOut.toString();
}
// Draw a filled rectangle
private void drawFilledRectangle(DrawContext dc, double x, double y, double width, double height)
{
GL gl = dc.getGL();
gl.glBegin(GL.GL_POLYGON);
gl.glVertex3d(x, y, 0);
gl.glVertex3d(x + width - 1, y, 0);
gl.glVertex3d(x + width - 1, y + height - 1, 0);
gl.glVertex3d(x, y + height - 1, 0);
gl.glVertex3d(x, y, 0);
gl.glEnd();
}
private Color applyTextAlpha(Color color)
{
return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() * textColor.getAlpha() / 255 );
}
// -- Draw state handling -----------------------------------
private class DrawState
{
private class DrawAttributes
{
private final Font font;
private String hyperlink;
private final Color color;
public DrawAttributes(Font font, String hyperlink, Color color)
{
this.font = font;
this.hyperlink = hyperlink;
this.color = color;
}
}
private ArrayList<DrawAttributes> stack = new ArrayList<DrawAttributes>();
private TextRendererCache renderers;
public TextRenderer textRenderer;
public DrawState(TextRendererCache renderers, Font font, String hyperlink, Color color)
{
this.stack.add(new DrawAttributes(font, hyperlink, color));
this.renderers = renderers;
this.textRenderer = getTextRenderer(font);
}
public DrawState(DrawState ds)
{
for (DrawAttributes da : ds.stack)
this.stack.add(new DrawAttributes(da.font, da.hyperlink, da.color));
this.renderers = ds.renderers;
this.textRenderer = ds.textRenderer;
}
public DrawAttributes getDrawAttributes()
{
if (this.stack.size() < 1)
return null;
return this.stack.get(this.stack.size() - 1);
}
private TextRenderer getTextRenderer(Font font)
{
TextRenderer tr = this.renderers.get(font);
if(tr == null)
{
tr = new TextRenderer(font, true, true);
tr.setUseVertexArrays(false);
renderers.add(font, tr);
}
return tr;
}
private Font getFont(Font font, boolean isBold, boolean isItalic)
{
int fontStyle = isBold ? (isItalic ? Font.BOLD | Font.ITALIC : Font.BOLD)
: (isItalic ? Font.ITALIC : Font.PLAIN);
return font.deriveFont(fontStyle);
}
// Update DrawState from html tag
public TextRenderer update(String tag, boolean startStopRendering)
{
DrawAttributes da = getDrawAttributes();
boolean fontChanged = false;
if(tag.equalsIgnoreCase("<b>"))
{
this.stack.add(new DrawAttributes(getFont(da.font, true, da.font.isItalic()), da.hyperlink, da.color));
fontChanged = true;
}
else if(tag.equalsIgnoreCase("</b>"))
{
if (this.stack.size() > 1)
this.stack.remove(this.stack.size() - 1);
fontChanged = true;
}
else if(tag.equalsIgnoreCase("<i>"))
{
this.stack.add(new DrawAttributes(getFont(da.font, da.font.isBold(), true), da.hyperlink, da.color));
fontChanged = true;
}
else if(tag.equalsIgnoreCase("</i>"))
{
if (this.stack.size() > 1)
this.stack.remove(this.stack.size() - 1);
fontChanged = true;
}
else if(tag.toLowerCase().startsWith("<a "))
{
this.stack.add(new DrawAttributes(da.font, MultiLineTextRenderer.getAttributeFromTagHTML(tag, "href"), applyTextAlpha(linkColor)));
if(startStopRendering)
this.textRenderer.setColor(applyTextAlpha(linkColor));
}
else if(tag.equalsIgnoreCase("</a>"))
{
if (this.stack.size() > 1)
this.stack.remove(this.stack.size() - 1);
if(startStopRendering)
this.textRenderer.setColor(getDrawAttributes().color);
}
else if(tag.toLowerCase().startsWith("<font "))
{
String colorCode = MultiLineTextRenderer.getAttributeFromTagHTML(tag, "color");
if (colorCode != null)
{
Color color = da.color;
try
{
color = applyTextAlpha(Color.decode(colorCode));
}
catch (Exception e) {}
this.stack.add(new DrawAttributes(da.font, da.hyperlink, color));
if(startStopRendering)
this.textRenderer.setColor(color);
}
}
else if(tag.equalsIgnoreCase("</font>"))
{
if (this.stack.size() > 1)
this.stack.remove(this.stack.size() - 1);
if(startStopRendering)
this.textRenderer.setColor(getDrawAttributes().color);
}
if(fontChanged)
{
// Terminate current rendering
if(startStopRendering)
this.textRenderer.end3DRendering();
// Get new text renderer
da = getDrawAttributes();
this.textRenderer = getTextRenderer(da.font);
// Resume rendering
if(startStopRendering)
{
this.textRenderer.begin3DRendering();
this.textRenderer.setColor(da.color);
}
}
return this.textRenderer;
}
}
}