Update to Worldwind release 0.4.1
[worldwind-tracker.git] / gov / nasa / worldwind / render / MultiLineTextRenderer.java
blob3d334b236a60dd841edd631f81d0f36873e56e4a
1 /*
2 Copyright (C) 2001, 2006, 2007 United States Government
3 as represented by the Administrator of the
4 National Aeronautics and Space Administration.
5 All Rights Reserved.
6 */
7 package gov.nasa.worldwind.render;
9 import com.sun.opengl.util.j2d.TextRenderer;
10 import gov.nasa.worldwind.util.Logging;
11 import gov.nasa.worldwind.pick.PickSupport;
12 import gov.nasa.worldwind.pick.PickedObject;
13 import gov.nasa.worldwind.geom.Position;
14 import gov.nasa.worldwind.avlist.AVKey;
16 import javax.media.opengl.GL;
17 import java.awt.*;
18 import java.awt.geom.Rectangle2D;
19 import java.util.regex.Pattern;
20 import java.util.regex.Matcher;
21 import java.util.HashMap;
22 import java.util.ArrayList;
24 /**
25 * Multi line, rectangle bound text renderer with (very) minimal html support.
26 *<p>
27 * The {@link MultiLineTextRenderer} (MLTR) handles wrapping, measuring and drawing
28 * of multiline text strings using Sun's JOGL {@link TextRenderer}.
29 *</p>
30 * <p>
31 * A multiline text string is a character string containing new line characters
32 * in between lines.
33 * </p>
34 * <p>
35 * MLTR can handle both regular text with new line seprators and a very minimal
36 * implementation of HTML. Each type of text has its own methods though.
37 *</p>
39 * <p><b>Usage:</b></p>
41 * <p>Instantiation:</p>
42 * <p>
43 * The MLTR needs a Font or a TextRenderer to be instanciated. This will be
44 * the font used for text drawing, wrapping and measuring. For HTML methods
45 * this font will be considered as the document default font.
46 * </p>
47 * <pre>
48 * Font font = Font.decode("Arial-PLAIN-12");
49 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(font);
50 * </pre>
51 * or
52 * <pre>
53 * TextRenderer tr = new TextRenderer(Font.decode("Arial-PLAIN-10"));
54 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(tr);
55 * </pre>
57 * <p>Drawing regular text:</p>
58 * <pre>
59 * String text = "Line one.\nLine two.\nLine three...";
60 * int x = 10; // Upper left corner of text rectangle.
61 * int y = 200; // Origin at bottom left of screen.
62 * int lineHeight = 14; // Line height in pixels.
63 * Color color = Color.RED;
65 * mltr.setTextColor(color);
66 * mltr.getTextRenderer().begin3DRendering();
67 * mltr.draw(text, x, y, lineHeight);
68 * mltr.getTextRenderer().end3DRendering();
69 * </pre>
71 * <p>Wrapping text to fit inside a width and optionaly a height</p>
72 * <p>
73 * The MLTR wrap method will insert new line characters inside the text so that
74 * it fits a given width in pixels.
75 * </p>
76 * <p>
77 * If a height dimension above zero is specified too, the text will be truncated
78 * if needed, and a continuation string will be appended to the last line. The
79 * continuation string can be set with mltr.setContinuationString();
80 * </p>
81 * <pre>
82 * // Fit inside 300 pixels, no height constraint
83 * String wrappedText = mltr.wrap(text, new Dimension(300, 0));
85 * // Fit inside 300x400 pixels, text may be truncated
86 * String wrappedText = mltr.wrap(text, new Dimension(300, 400));
87 * </pre>
89 * <p>Measuring text</p>
90 * <pre>
91 * Rectangle2D textBounds = mltr.getBounds(text);
92 * </pre>
93 * <p>
94 * The textBounds rectangle returned contains the width and height of the text
95 * as it would be drawn with the current font.
96 * </p>
97 * <p>
98 * Note that textBounds.minX is the number of lines found and textBounds.minY
99 * is the maximum line height for the font used. This value can be safely used
100 * as the lineHeight argument when drawing - or can even be ommited after a
101 * getBounds: draw(text, x, y);
102 * ...
103 * </p>
105 * <p><b>HTML support</b></p>
106 * <p>
107 * Supported tags are:
108 * <ul>
109 * <li>&lt;p&gt;&lt;/p&gt;, &lt;br&gt; &lt;br /&gt;</li>
110 * <li>&lt;b&gt;&lt;/b&gt;</li>
111 * <li>&lt;i&gt;&lt;/i&gt;</li>
112 * <li>&lt;a href="..."&gt;&lt;/a&gt;</li>
113 * <li>&lt;font color="#ffffff"&gt;&lt;/font&gt;</li>
114 * </ul>
115 * </p>
116 * ...
120 * <p>
121 * See {@link AbstractAnnotation}.drawAnnotation() for more usage details.
122 * </p>
124 * @author: Patrick Murris
125 * @version $Id$
127 public class MultiLineTextRenderer
129 public final static int ALIGN_LEFT = 0;
130 public final static int ALIGN_CENTER = 1;
131 public final static int ALIGN_RIGHT = 2;
133 public static final String EFFECT_NONE = "render.MultiLineTextRenderer.EffectNone";
134 public static final String EFFECT_SHADOW = "render.MultiLineTextRenderer.EffectShadow";
135 public static final String EFFECT_OUTLINE = "render.MultiLineTextRenderer.EffectOutline";
137 private TextRenderer textRenderer;
138 private int lineSpacing = 0; // Inter line spacing in pixels
139 private int lineHeight = 14; // Will be set by getBounds() or by application
140 private int textAlign = ALIGN_LEFT; // Text alignement
141 private String continuationString = "...";
142 private Color textColor = Color.DARK_GRAY;
143 private Color backColor = Color.LIGHT_GRAY;
144 private Color linkColor = Color.BLUE;
146 public MultiLineTextRenderer(TextRenderer textRenderer)
148 if(textRenderer == null)
150 String msg = Logging.getMessage("nullValue.TextRendererIsNull");
151 Logging.logger().severe(msg);
152 throw new IllegalArgumentException(msg);
154 this.textRenderer = textRenderer;
157 public MultiLineTextRenderer(Font font)
159 if(font == null)
161 String msg = Logging.getMessage("nullValue.FontIsNull");
162 Logging.logger().severe(msg);
163 throw new IllegalArgumentException(msg);
165 this.textRenderer = new TextRenderer(font, true, true);
169 * Get the current TextRenderer.
170 * @return the current TextRenderer.
172 public TextRenderer getTextRenderer()
174 return this.textRenderer;
178 * Get the current line spacing height in pixels.
179 * @return the current line spacing height in pixels.
181 public int getLineSpacing()
183 return this.lineSpacing;
187 * Set the current line spacing height in pixels.
188 * @param height the line spacing height in pixels.
190 public void setLineSpacing(int height)
192 this.lineSpacing = height;
196 * Get the current line height in pixels.
197 * @return the current line height in pixels.
199 public int getLineHeight()
201 return this.lineHeight;
205 * Set the current line height in pixels.
206 * @param height the current line height in pixels.
208 public void setLineHeight(int height)
210 this.lineHeight = height;
214 * Get the current text alignment. Can be one of {@link #ALIGN_LEFT} the default,
215 * {@link #ALIGN_CENTER} or {@link #ALIGN_RIGHT}.
216 * @return the current text alignment.
218 public int getTextAlign()
220 return this.textAlign;
224 * Set the current text alignment. Can be one of {@link #ALIGN_LEFT} the default,
225 * {@link #ALIGN_CENTER} or {@link #ALIGN_RIGHT}.
226 * @param align the current text alignment.
228 public void setTextAlign(int align)
230 this.textAlign = align;
234 * Get the current text color.
235 * @return the current text color.
237 public Color getTextColor()
239 return this.textColor;
243 * Set the text renderer color.
244 * @param color the color to use when drawing text.
246 public void setTextColor(Color color)
248 if(color != null)
250 this.textColor = color;
251 this.textRenderer.setColor(color);
256 * Get the background color used for EFFECT_SHADOW and EFFECT_OUTLINE.
257 * @return the current background color used when drawing shadow or outline..
259 public Color getBackColor()
261 return this.backColor;
265 * Set the background color used for EFFECT_SHADOW and EFFECT_OUTLINE.
266 * @param color the color to use when drawing shadow or outline.
268 public void setBackColor(Color color)
270 if(color != null)
272 this.backColor = color;
277 * Get the current link color.
278 * @return the current link color.
280 public Color getLinkColor()
282 return this.linkColor;
286 * Set the link color.
287 * @param color the color to use when drawing hyperlinks.
289 public void setLinkColor(Color color)
291 if(color != null)
293 this.linkColor = color;
298 * Set the character string appended at the end of text truncated during
299 * a wrap operation when exceeding the given height limit.
300 * @param s the continuation character string.
302 public void setContinuationString(String s)
304 this.continuationString = s;
308 * Get the maximum line height for the given text renderer.
309 * @param tr the TextRenderer.
310 * @return the maximum line height.
312 public double getMaxLineHeight(TextRenderer tr)
314 // Check underscore + capital E with acute accent
315 return tr.getBounds("_\u00c9").getHeight();
319 //** Plain text support ******************************************************
320 //****************************************************************************
323 * Returns the bounding rectangle for a multi-line string.
324 * Note that the X component of the rectangle is the number of lines found in the text
325 * and the Y component of the rectangle is the max line height encountered.
326 * Note too that this method will automatically set the current line height to the max height found.
327 * @param text the multi-line text to evaluate.
328 * @return the bounding rectangle for the string.
330 public Rectangle getBounds(String text)
332 int width = 0;
333 int maxLineHeight = 0;
334 String[] lines = text.split("\n");
335 for(int i = 0; i < lines.length; i++)
337 Rectangle2D lineBounds = this.textRenderer.getBounds(lines[i]);
338 width = (int)Math.max(lineBounds.getWidth(), width);
339 maxLineHeight = (int)Math.max(lineBounds.getHeight(), lineHeight);
341 // Make sure we have the highest line height
342 maxLineHeight = (int)Math.max(getMaxLineHeight(this.textRenderer), maxLineHeight);
343 // Set current line height for future draw
344 this.lineHeight = maxLineHeight;
345 // Compute final height using maxLineHeight and number of lines
346 return new Rectangle(lines.length, lineHeight, width,
347 lines.length * maxLineHeight + (lines.length - 1) * this.lineSpacing);
351 * Draw a multi-line text string with bounding rectangle top starting at the y position.
352 * Depending on the current textAlign, the x position is either the rectangle left side,
353 * middle or right side.
354 * Uses the current line height.
355 * @param text the multi-line text to draw.
356 * @param x the x position for top left corner of text rectangle.
357 * @param y the y position for top left corner of the text rectangle.
359 public void draw(String text, int x, int y)
361 this.draw(text, x, y, this.lineHeight);
364 public void draw(String text, int x, int y, String effect)
366 this.draw(text, x, y, this.lineHeight, effect);
369 public void draw(String text, int x, int y, int textLineHeight, String effect)
371 if (effect.compareToIgnoreCase(EFFECT_SHADOW) == 0)
373 this.textRenderer.setColor(backColor);
374 this.draw(text, x + 1, y - 1, textLineHeight);
375 this.textRenderer.setColor(textColor);
377 else if (effect.compareToIgnoreCase(EFFECT_OUTLINE) == 0)
379 this.textRenderer.setColor(backColor);
380 this.draw(text, x, y + 1, textLineHeight);
381 this.draw(text, x + 1, y, textLineHeight);
382 this.draw(text, x, y - 1, textLineHeight);
383 this.draw(text, x - 1, y, textLineHeight);
384 this.textRenderer.setColor(textColor);
386 // Draw normal text
387 this.draw(text, x, y, textLineHeight);
391 * Draw a multi-line text string with bounding rectangle top starting at the y position.
392 * Depending on the current textAlign, the x position is either the rectangle left side,
393 * middle or right side.
394 * Uses the given line height.
395 * @param text the multi-line text to draw.
396 * @param x the x position for top left corner of text rectangle.
397 * @param y the y position for top left corner of the text rectangle.
398 * @param textLineHeight the line height in pixels.
400 public void draw(String text, int x, int y, int textLineHeight)
402 String[] lines = text.split("\n");
403 for(int i = 0; i < lines.length; i++)
405 int xAligned = x;
406 if(this.textAlign == ALIGN_CENTER)
407 xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth() / 2);
408 else if(this.textAlign == ALIGN_RIGHT)
409 xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth());
410 y -= textLineHeight;
411 this.textRenderer.draw(lines[i], xAligned, y);
412 y -= this.lineSpacing;
417 * Draw text with unique colors word bounding rectangles and add each as a pickable object
418 * to the provided PickSupport instance.
419 * @param text the multi-line text to draw.
420 * @param x the x position for top left corner of text rectangle.
421 * @param y the y position for top left corner of the text rectangle.
422 * @param textLineHeight the line height in pixels.
423 * @param dc the current DrawContext.
424 * @param pickSupport the PickSupport instance to be used.
425 * @param refObject the user reference object associated with every picked word.
426 * @param refPosition the user reference Position associated with every picked word.
428 public void pick(String text, int x, int y, int textLineHeight,
429 DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
431 String[] lines = text.split("\n");
432 for(int i = 0; i < lines.length; i++)
434 int xAligned = x;
435 if(this.textAlign == ALIGN_CENTER)
436 xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth() / 2);
437 else if(this.textAlign == ALIGN_RIGHT)
438 xAligned = x - (int)(this.textRenderer.getBounds(lines[i]).getWidth());
439 y -= textLineHeight;
440 drawLineWithUniqueColors(lines[i], xAligned, y, dc, pickSupport, refObject, refPosition);
441 y -= this.lineSpacing;
445 private void drawLineWithUniqueColors(String text, int x, int y,
446 DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
448 float spaceWidth = this.textRenderer.getCharWidth(' ');
449 float drawX = x;
450 float drawY = y;
451 String source = text.trim();
452 int start = 0;
453 int end = source.indexOf(' ', start + 1);
454 while(start < source.length())
456 if(end == -1)
457 end = source.length(); // last word
458 // Extract a 'word' which is in fact a space and a word except for first word
459 String word = source.substring(start, end);
460 // Measure word and already draw line part - from line beginning
461 Rectangle2D wordBounds = this.textRenderer.getBounds(word);
462 Rectangle2D drawnBounds = this.textRenderer.getBounds(source.substring(0, start));
463 float space = word.charAt(0) == ' ' ? spaceWidth : 0f;
464 drawX = x + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
465 // Add pickable object
466 Color color = dc.getUniquePickColor();
467 int colorCode = color.getRGB();
468 PickedObject po = new PickedObject(colorCode, refObject, refPosition, false);
469 po.setValue(AVKey.TEXT, word.trim());
470 pickSupport.addPickableObject(colorCode, po);
471 // Draw word rectangle
472 dc.getGL().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
473 drawFilledRectangle(dc, drawX + wordBounds.getX(), drawY - wordBounds.getHeight() - wordBounds.getY(),
474 wordBounds.getWidth(), wordBounds.getHeight());
475 // Move forward in source string
476 start = end;
477 if(start < source.length() - 1)
479 end = source.indexOf(' ', start + 1);
485 * Add 'new line' characters inside a string so that it's bounding rectangle
486 * tries not to exceed the given dimension width.
487 * If the dimension height is more than zero, the text will be truncated accordingly and
488 * the continuation string will be appended to the last line.
489 * Note that words will not be split and at least one word will be used per line
490 * so the longest word defines the final width of the bounding rectangle.
491 * Each line is trimmed of leading and trailing spaces.
492 * @param text the text string to wrap
493 * @param dimension the maximum dimension in pixels
494 * @return the wrapped string
496 public String wrap(String text, Dimension dimension)
498 int width = (int)dimension.getWidth();
499 int height = (int)dimension.getHeight();
500 String[] lines = text.split("\n");
501 StringBuffer wrappedText = new StringBuffer();
502 // Wrap each line
503 for(int i = 0; i < lines.length; i++)
505 lines[i] = this.wrapLine(lines[i], width);
507 // Concatenate all lines in one string with new line separators
508 // between lines - not at the end
509 // Checks for height limit.
510 int currentHeight = 0;
511 boolean heightExceeded = false;
512 double maxLineHeight = getMaxLineHeight(this.textRenderer);
513 for(int i = 0; i < lines.length && !heightExceeded; i++)
515 String[] subLines = lines[i].split("\n");
516 for(int j = 0; j < subLines.length && !heightExceeded; j++)
518 if(height <= 0 || currentHeight + maxLineHeight <= height)
520 wrappedText.append(subLines[j]);
521 currentHeight += maxLineHeight + this.lineSpacing;
522 if(j < subLines.length - 1)
523 wrappedText.append('\n');
525 else
527 heightExceeded = true;
530 if(i < lines.length - 1 && !heightExceeded)
531 wrappedText.append('\n');
533 // Add continuation string if text truncated
534 if(heightExceeded)
536 if(wrappedText.length() > 0)
537 wrappedText.deleteCharAt(wrappedText.length() - 1); // Remove excess new line
538 wrappedText.append(this.continuationString);
540 return wrappedText.toString();
543 // Wrap one line to fit the given width
544 private String wrapLine(String text, int width)
546 StringBuffer wrappedText = new StringBuffer();
547 // Single line - trim leading and trailing spaces
548 String source = text.trim();
549 Rectangle2D lineBounds = this.textRenderer.getBounds(source);
550 if(lineBounds.getWidth() > width)
552 // Split single line to fit preferred width
553 StringBuffer line = new StringBuffer();
554 int start = 0;
555 int end = source.indexOf(' ', start + 1);
556 while(start < source.length())
558 if(end == -1)
559 end = source.length(); // last word
560 // Extract a 'word' which is in fact a space and a word
561 String word = source.substring(start, end);
562 String linePlusWord = line + word;
563 if(this.textRenderer.getBounds(linePlusWord).getWidth() <= width)
565 // Keep adding to the current line
566 line.append(word);
568 else
570 // Width exceeded
571 if(line.length() != 0 )
573 // Finish current line and start new one
574 wrappedText.append(line);
575 wrappedText.append('\n');
576 line.delete(0, line.length());
577 line.append(word.trim()); // get read of leading space(s)
579 else
581 // Line is empty, force at least one word
582 line.append(word.trim());
585 // Move forward in source string
586 start = end;
587 if(start < source.length() - 1)
589 end = source.indexOf(' ', start + 1);
592 // Gather last line
593 wrappedText.append(line);
595 else
597 // Line doesnt need to be wrapped
598 wrappedText.append(source);
600 return wrappedText.toString();
603 //** Very very simple html support *******************************************
604 // Handles <P></P>, <BR /> or <BR>, <B></B>, <I></I>, <A HREF="..."></A>
605 // and <font color="#ffffff"></font>.
606 //****************************************************************************
610 * Return true if the text contains some sgml tags.
611 * @param text The text string to evaluate.
612 * @return true if the string contains sgml or html tags
614 public static boolean containsHTML(String text)
616 Pattern pattern = Pattern.compile("<[^\\s].*?>"); // Match any sgml tag
617 Matcher matcher = pattern.matcher(text);
618 return matcher.find();
622 * Remove new line characters then replace BR and P tags with appropriate new lines
623 * @param text The html text string to process.
624 * @return The processed text string.
626 public static String processLineBreaksHTML(String text)
628 text = text.replaceAll("\n", ""); // Remove all new line characters
629 text = text.replaceAll("(?i)<br\\s?.*?>", "\n"); // Replace <br ...> with one new line
630 text = text.replaceAll("(?i)<p\\s?.*?>", ""); // Replace <p ...> with nothing
631 text = text.replaceAll("(?i)</p>", "\n\n"); // Replace </p> with two new line
632 return text;
636 * Remove all HTML tags from a text string.
637 * @param text the string to filter.
638 * @return the filtered string.
640 public static String removeTagsHTML(String text)
642 return text.replaceAll("<[^\\s].*?>", "");
646 * Extract an attribute value from a HTML tag string. The attribute is expected to be formed
647 * on the pattern: name="...". Other variants will likely fail.
648 * @param text the HTML tage string.
649 * @param attributeName the attribute name.
650 * @return the attribute value found. Null if empty or not found.
652 public static String getAttributeFromTagHTML(String text, String attributeName)
654 // Look for name="..." - will not work for other variants
655 Pattern pattern = Pattern.compile("(?i)" + attributeName.toLowerCase() + "=\"([^\"].*?)\"");
656 Matcher matcher = pattern.matcher(text);
657 if (matcher.find())
658 return matcher.group(1);
660 return null;
664 * Returns the bounding rectangle for a multi-line html string.
665 * Note that the X component of the rectangle is the number of lines found in the text
666 * and the Y component of the rectangle is the average line height encountered.
667 * @param text the multi-line html text to evaluate.
668 * @param renderers A HashMap of fonts and shared text renderers.
669 * @return the bounding rectangle for the rendered text.
671 public Rectangle2D getBoundsHTML(String text, TextRendererCache renderers)
673 DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
674 return getBoundsHTML(text, renderers, ds);
678 * Returns the bounding rectangle for a multi-line html string.
679 * Note that the X component of the rectangle is the number of lines found in the text
680 * and the Y component of the rectangle is the average line height encountered.
681 * @param text the multi-line html text to evaluate.
682 * @param renderers A HashMap of fonts and shared text renderers.
683 * @param dsCurrent The current DrawState.
684 * @return the bounding rectangle for the rendered text.
686 public Rectangle2D getBoundsHTML(String text, TextRendererCache renderers,
687 DrawState dsCurrent)
689 String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
690 Pattern pattern = Pattern.compile(regex);
692 // Use a copy of DrawState - do not alter original
693 DrawState ds = new DrawState(dsCurrent);
695 // Spilt string
696 double width = 0;
697 double height = 0;
698 String[] lines = text.split("\n");
699 StringBuffer linePart = new StringBuffer();
700 for(int i = 0; i < lines.length; i++)
702 // Measure each line
703 int start = 0;
704 double lineWidth = 0;
705 double maxLineHeight = getMaxLineHeight(ds.textRenderer);
706 linePart.delete(0, linePart.length());
707 Matcher matcher = pattern.matcher(lines[i]);
708 while (matcher.find()) {
709 if(matcher.group().compareTo(" ") == 0)
711 // Space found, concatenate and keep going
712 linePart.append(lines[i].substring(start, matcher.start()));
713 start = matcher.start(); // move on
715 else
717 // Html tag found
719 // Process current line part and measure - use counterTrim() workaround
720 linePart.append(lines[i].substring(start, matcher.start()));
721 if(linePart.length() > 0)
723 Rectangle2D partBounds = ds.textRenderer.getBounds(counterTrim(linePart));
724 //Rectangle2D partBounds = currentTextRenderer.getBounds(linePart);
725 lineWidth += partBounds.getWidth() + partBounds.getX();
726 linePart.delete(0, linePart.length()); // clear part
728 start = matcher.end(); // move on
730 // Process html tag and update draw attributes
731 ds.update(matcher.group(), false);
733 // Keep track of max line height
734 maxLineHeight = (int)Math.max(getMaxLineHeight(ds.textRenderer), maxLineHeight);
738 // Gather and measure end of line
739 if(start < lines[i].length())
741 linePart.append(lines[i].substring(start));
742 if(linePart.length() > 0)
744 //Rectangle2D partBounds = currentTextRenderer.getBounds(counterTrim(linePart));
745 Rectangle2D partBounds = ds.textRenderer.getBounds(linePart);
746 lineWidth += partBounds.getWidth() + partBounds.getX();
747 maxLineHeight = (int)Math.max(partBounds.getHeight(), maxLineHeight);
751 // Accumulate dimensions
752 width = Math.max(width, lineWidth);
753 height += maxLineHeight + this.lineSpacing;
756 height -= this.lineSpacing; // subtract last line spacing
757 // Return bounds - Note that minX is the number of lines and minY is the line height average
758 return new Rectangle(lines.length, (int)(height / lines.length),
759 (int)Math.round(width), (int)Math.round(height));
763 * Draw a multi-line html text string with bounding rectangle top starting at the y position. The x
764 * position is eiher the rectangle left side, middle or right side depending on the current text alignement.
765 * @param text the multi-line text to draw
766 * @param x the x position for top left corner of text rectangle
767 * @param y the y position for top left corner of the text rectangle
768 * @param renderers A HashMap of fonts and shared text renderers.
770 public void drawHTML(String text, int x, int y, TextRendererCache renderers)
772 String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
773 Pattern pattern = Pattern.compile(regex);
775 // Draw attributes
776 DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
778 // Draw string
779 int baseX = x;
780 double drawY = y;
781 ds.textRenderer.begin3DRendering();
782 ds.textRenderer.setColor(this.textColor);
783 String[] lines = text.split("\n");
784 StringBuffer linePart = new StringBuffer();
785 for(int i = 0; i < lines.length; i++)
787 // Set line start x
788 double drawX = baseX;
789 Rectangle2D lineBounds = getBoundsHTML(lines[i], renderers, ds);
790 if(this.textAlign == ALIGN_CENTER)
791 drawX = x - (int)(lineBounds.getWidth() / 2);
792 else if(this.textAlign == ALIGN_RIGHT)
793 drawX = x - (int)(lineBounds.getWidth());
794 // Skip line height
795 drawY -= lineBounds.getHeight();
797 // Draw one line
798 int start = 0;
799 linePart.delete(0, linePart.length());
800 Matcher matcher = pattern.matcher(lines[i]);
801 while (matcher.find()) {
802 if(matcher.group().compareTo(" ") == 0)
804 // Space found, concatenate and keep going
805 linePart.append(lines[i].substring(start, matcher.start()));
806 start = matcher.start(); // move on
808 else
810 // Html tag found
812 // Process current line part and draw
813 linePart.append(lines[i].substring(start, matcher.start()));
814 if(linePart.length() > 0)
816 // Draw
817 ds.textRenderer.draw(linePart, (int)Math.round(drawX), (int)Math.round(drawY));
819 // Move x - use antiTrim() workaround
820 Rectangle2D partBounds = ds.textRenderer.getBounds(counterTrim(linePart));
821 //Rectangle2D partBounds = currentTextRenderer.getBounds(linePart);
822 drawX += partBounds.getWidth() + partBounds.getX();
824 linePart.delete(0, linePart.length()); // clear part
826 start = matcher.end(); // move on
828 // Process html tag and update draw attributes
829 ds.update(matcher.group(), true);
833 // Gather and draw end of line
834 if(start < lines[i].length())
836 linePart.append(lines[i].substring(start));
837 if(linePart.length() > 0)
838 ds.textRenderer.draw(linePart, (int)Math.round(drawX), (int)Math.round(drawY));
840 // Skip line spacing
841 drawY -= this.lineSpacing;
843 ds.textRenderer.end3DRendering();
847 * Draw text with unique colors word bounding rectangles and add each as a pickable object
848 * to the provided PickSupport instance.
849 * @param text the multi-line text to draw.
850 * @param x the x position for top left corner of text rectangle.
851 * @param y the y position for top left corner of the text rectangle.
852 * @param renderers A HashMap of fonts and shared text renderers.
853 * @param dc the current DrawContext.
854 * @param pickSupport the PickSupport instance to be used.
855 * @param refObject the user reference object associated with every picked word.
856 * @param refPosition the user reference Position associated with every picked word.
858 public void pickHTML(String text, int x, int y, TextRendererCache renderers,
859 DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
861 String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
862 Pattern pattern = Pattern.compile(regex);
864 // Draw attributes
865 DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
867 // Draw string
868 double drawX = x;
869 double drawY = y;
870 String[] lines = text.split("\n");
871 StringBuffer linePart = new StringBuffer();
872 for(int i = 0; i < lines.length; i++)
874 // Set line start x
875 double baseX = x;
876 Rectangle2D lineBounds = getBoundsHTML(lines[i], renderers, ds);
877 if(this.textAlign == ALIGN_CENTER)
878 baseX = x - (int)(lineBounds.getWidth() / 2);
879 else if(this.textAlign == ALIGN_RIGHT)
880 baseX = x - (int)(lineBounds.getWidth());
881 // Skip line height
882 drawY -= lineBounds.getHeight();
884 // Save draw state at beginning of line and word
885 DrawState dsLine = new DrawState(ds);
886 DrawState dsWord = new DrawState(ds);
888 // Draw one line
889 int wordStart = -1;
890 int start = 0;
891 linePart.delete(0, linePart.length());
892 Matcher matcher = pattern.matcher(lines[i]);
893 while (matcher.find()) {
894 if(matcher.group().compareTo(" ") == 0)
896 // Space found - get and measure new word and already drawn part
897 String word = wordStart == -1 ? lines[i].substring(start, matcher.start())
898 : lines[i].substring(wordStart, matcher.start());
899 String drawn = wordStart == -1 ? lines[i].substring(0, start)
900 : lines[i].substring(0, wordStart);
901 Rectangle2D wordBounds = getBoundsHTML(word, renderers, dsWord);
902 Rectangle2D drawnBounds = getBoundsHTML(drawn, renderers, dsLine);
904 // get current hyperlink
905 String hyperlink = dsWord.getDrawAttributes().hyperlink != null ? dsWord.getDrawAttributes().hyperlink : ds.getDrawAttributes().hyperlink;
906 // Draw word bounding rectangle
907 drawX = baseX + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
908 pickWord( word, hyperlink, drawX, drawY, wordBounds, dc, pickSupport, refObject, refPosition);
910 // Save draw state for next word
911 dsWord = new DrawState(ds);
913 start = matcher.start(); // move on from space found
914 wordStart = -1;
916 else
918 // Html tag found
920 wordStart = wordStart == -1 ? start : wordStart;
921 start = matcher.end(); // move on from after tag
923 // Process html tag and update draw attributes
924 ds.update(matcher.group(), false);
928 // Gather and draw end of line
929 if(start < lines[i].length() || wordStart != -1)
931 String word = wordStart == -1 ? lines[i].substring(start) : lines[i].substring(wordStart);
932 String drawn = wordStart == -1 ? lines[i].substring(0, start) : lines[i].substring(0, wordStart);
933 Rectangle2D wordBounds = getBoundsHTML(word, renderers, dsWord);
934 Rectangle2D drawnBounds = getBoundsHTML(drawn, renderers, dsLine);
936 // get current hyperlink
937 String hyperlink = dsWord.getDrawAttributes().hyperlink != null ? dsWord.getDrawAttributes().hyperlink : ds.getDrawAttributes().hyperlink;
938 // Draw word bounding rectangle
939 drawX = baseX + (start > 0 ? (float)drawnBounds.getWidth() + (float)drawnBounds.getX() : 0);
940 pickWord( word, hyperlink, drawX, drawY, wordBounds, dc, pickSupport, refObject, refPosition);
942 // Skip line spacing
943 drawY -= this.lineSpacing;
947 private void pickWord(String word, String hyperlink, double drawX, double drawY, Rectangle2D wordBounds,
948 DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition)
950 // Add pickable object
951 Color color = dc.getUniquePickColor();
952 int colorCode = color.getRGB();
953 PickedObject po = new PickedObject(colorCode, refObject, refPosition, false);
954 po.setValue(AVKey.TEXT, removeTagsHTML(word.trim()));
955 if(hyperlink != null)
956 po.setValue(AVKey.URL, hyperlink);
957 pickSupport.addPickableObject(colorCode, po);
958 // Draw word rectangle
959 dc.getGL().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
960 drawFilledRectangle(dc, drawX, drawY - wordBounds.getHeight() / 5,
961 wordBounds.getWidth(), wordBounds.getHeight());
966 * Add 'new line' characters inside an html text string so that it's bounding rectangle
967 * tries not to exceed the given dimension width.
968 * If the dimension height is more than zero, the text will be truncated accordingly and
969 * the continuation string will be appended to the last line.
970 * Note that words will not be split and at least one word will be used per line
971 * so the longest word defines the final width of the bounding rectangle.
972 * Each line is trimmed of leading and trailing spaces.
973 * @param text the html text string to wrap
974 * @param dimension the maximum dimension in pixels
975 * @param renderers A HashMap of fonts and shared text renderers.
976 * @return the wrapped html string
978 public String wrapHTML(String text, Dimension dimension, TextRendererCache renderers)
980 String regex = "(<[^\\s].*?>)|(\\s)"; // Find sgml tags or spaces
981 Pattern pattern = Pattern.compile(regex);
983 // Draw attributes
984 DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor);
985 int width = (int)dimension.getWidth();
986 int height = (int)dimension.getHeight();
988 // Split string
989 String[] lines = text.split("\n");
990 StringBuffer wrappedText = new StringBuffer();
991 int currentHeight = 0;
992 int lineCount = 0;
993 boolean heightExceeded = false;
994 for(int i = 0; i < lines.length && !heightExceeded; i++)
996 // Single line - trim leading and trailing spaces
997 String source = lines[i].trim();
998 double maxLineHeight = getMaxLineHeight(ds.textRenderer);
999 Rectangle2D lineBounds = getBoundsHTML(source, renderers, ds);
1000 if(lineBounds.getWidth() > width)
1002 // Split single line to fit preferred width
1004 StringBuffer line = new StringBuffer();
1005 double lineWidth = 0;
1006 double wordWidth = 0;
1007 int wordStart = -1;
1008 int start = 0;
1009 Matcher matcher = pattern.matcher(source);
1010 while (matcher.find() && !heightExceeded)
1012 if(matcher.group().compareTo(" ") == 0)
1014 // Space found - check new word length and line total
1015 String word = source.substring(start, matcher.start());
1016 Rectangle2D wordBounds = getBoundsHTML(word, renderers, ds);
1017 wordWidth += wordBounds.getWidth() + wordBounds.getX();
1018 // If word already started earlier, gather the full word
1019 if(wordStart != -1)
1020 word = source.substring(wordStart, matcher.start());
1021 if(lineWidth + wordWidth <= width)
1023 // Keep adding to the current line
1024 line.append(word);
1025 lineWidth += wordWidth;
1027 else
1029 // Width exceeded
1030 word = word.trim(); // get read of leading space(s)
1031 wordBounds = getBoundsHTML(word, renderers, ds);
1032 wordWidth = wordBounds.getWidth() + wordBounds.getX();
1033 if(line.length() != 0 )
1035 // Finish current line and start new one
1036 if(height <= 0 || currentHeight + maxLineHeight <= height)
1038 wrappedText.append(line);
1039 wrappedText.append('\n');
1040 currentHeight += maxLineHeight + this.lineSpacing;
1041 lineCount++;
1042 line.delete(0, line.length());
1043 line.append(word);
1044 lineWidth = wordWidth;
1045 // Keep track of max line height
1046 maxLineHeight = getMaxLineHeight(ds.textRenderer);
1048 else
1050 heightExceeded = true;
1053 else
1055 // Line is empty, force at least one word
1056 line.append(word);
1057 lineWidth = wordWidth;
1060 // Move on from space found
1061 start = matcher.start();
1062 wordWidth = 0;
1063 wordStart = -1;
1065 else
1067 // Html tag found
1069 // Process line part and measure - use counterTrim() workaround
1070 // Accumulate wordWidth and set wordStart to decide latter whether this is going on the current line
1071 if(matcher.start() > start)
1073 String word = source.substring(start, matcher.start());
1074 Rectangle2D wordBounds = getBoundsHTML(counterTrim(word), renderers, ds);
1075 wordWidth += wordBounds.getWidth() + wordBounds.getX();
1077 wordStart = wordStart == -1 ? start : wordStart;
1078 start = matcher.end(); // move on
1080 // Process html tag and update draw attributes
1081 ds.update(matcher.group(), false);
1082 // Keep track of max line height
1083 maxLineHeight = (int)Math.max(getMaxLineHeight(ds.textRenderer), maxLineHeight);
1086 // Gather and measure end of line if any
1087 if((start < source.length() || wordStart != -1) && !heightExceeded)
1089 String word = "";
1090 if(start < source.length())
1092 // Gather last bit and add to wordWidth
1093 word = source.substring(start);
1094 Rectangle2D wordBounds = getBoundsHTML(word, renderers, ds);
1095 wordWidth += wordBounds.getWidth() + wordBounds.getX();
1097 // If word already started earlier, gather the full word
1098 if(wordStart != -1)
1099 word = source.substring(wordStart);
1100 if(lineWidth + wordWidth <= width)
1102 // Keep adding to the current line
1103 line.append(word);
1105 else
1107 // Width exceeded
1108 word = word.trim(); // get read of leading space(s)
1109 if(line.length() != 0 )
1111 // Finish current line and start new one
1112 if(height <= 0 || currentHeight + maxLineHeight <= height)
1114 wrappedText.append(line);
1115 wrappedText.append('\n');
1116 currentHeight += maxLineHeight + this.lineSpacing;
1117 lineCount++;
1118 line.delete(0, line.length());
1119 line.append(word);
1120 // Keep track of max line height
1121 maxLineHeight = getMaxLineHeight(ds.textRenderer);
1123 else
1125 heightExceeded = true;
1128 else
1130 // Line is empty, force at least one word
1131 line.append(word);
1134 if(height <= 0 || currentHeight + maxLineHeight <= height)
1136 wrappedText.append(line);
1137 currentHeight += maxLineHeight + this.lineSpacing;
1138 lineCount++;
1140 else
1142 heightExceeded = true;
1146 else
1148 // line doesnt need to be wrapped
1149 if(height <= 0 || currentHeight + maxLineHeight <= height)
1151 wrappedText.append(source);
1152 currentHeight += maxLineHeight + this.lineSpacing;
1153 lineCount++;
1155 else
1157 heightExceeded = true;
1160 // Add new line between lines - not after the last one.
1161 if(i < lines.length - 1 && !heightExceeded)
1162 wrappedText.append('\n');
1164 // Add continuation string if text truncated
1165 if(heightExceeded)
1167 if(wrappedText.length() > 0)
1168 wrappedText.deleteCharAt(wrappedText.length() - 1); // Remove excess new line
1169 wrappedText.append(this.continuationString);
1171 return wrappedText.toString();
1174 // Replace first leading space and last trailing space with the character 't'.
1175 // This is a workaround for TextRenderer.getBounds() which ignores leading and trailing spaces.
1176 private String counterTrim(StringBuffer s)
1178 if(s.length() == 0)
1179 return "";
1181 StringBuffer sbOut = new StringBuffer(s);
1182 if(sbOut.substring(sbOut.length() - 1).compareTo(" ") == 0)
1183 sbOut.setCharAt(s.length() - 1, 't'); // use a 't' to fillup last space
1184 if(sbOut.substring(0, 1).compareTo(" ") == 0)
1185 sbOut.setCharAt(0, 't'); // use a 't' to fillup leading space
1187 return sbOut.toString();
1190 private String counterTrim(String s)
1192 if(s.length() == 0)
1193 return "";
1195 StringBuffer sbOut = new StringBuffer(s);
1196 if(sbOut.substring(sbOut.length() - 1).compareTo(" ") == 0)
1197 sbOut.setCharAt(s.length() - 1, 't'); // use a 't' to fillup last space
1198 if(sbOut.substring(0, 1).compareTo(" ") == 0)
1199 sbOut.setCharAt(0, 't'); // use a 't' to fillup leading space
1201 return sbOut.toString();
1204 // Draw a filled rectangle
1205 private void drawFilledRectangle(DrawContext dc, double x, double y, double width, double height)
1207 GL gl = dc.getGL();
1208 gl.glBegin(GL.GL_POLYGON);
1209 gl.glVertex3d(x, y, 0);
1210 gl.glVertex3d(x + width - 1, y, 0);
1211 gl.glVertex3d(x + width - 1, y + height - 1, 0);
1212 gl.glVertex3d(x, y + height - 1, 0);
1213 gl.glVertex3d(x, y, 0);
1214 gl.glEnd();
1217 private Color applyTextAlpha(Color color)
1219 return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() * textColor.getAlpha() / 255 );
1222 // -- Draw state handling -----------------------------------
1224 private class DrawState
1226 private class DrawAttributes
1228 private final Font font;
1229 private final String hyperlink;
1230 private final Color color;
1232 public DrawAttributes(Font font, String hyperlink, Color color)
1234 this.font = font;
1235 this.hyperlink = hyperlink;
1236 this.color = color;
1240 private ArrayList<DrawAttributes> stack = new ArrayList<DrawAttributes>();
1241 private TextRendererCache renderers;
1242 public TextRenderer textRenderer;
1244 public DrawState(TextRendererCache renderers, Font font, String hyperlink, Color color)
1246 this.stack.add(new DrawAttributes(font, hyperlink, color));
1247 this.renderers = renderers;
1248 this.textRenderer = getTextRenderer(font);
1251 public DrawState(DrawState ds)
1253 this.stack.addAll(ds.stack);
1254 this.renderers = ds.renderers;
1255 this.textRenderer = ds.textRenderer;
1258 public DrawAttributes getDrawAttributes()
1260 if (this.stack.size() < 1)
1261 return null;
1262 return this.stack.get(this.stack.size() - 1);
1265 private TextRenderer getTextRenderer(Font font)
1267 TextRenderer tr = this.renderers.get(font);
1268 if(tr == null)
1270 tr = new TextRenderer(font, true, true);
1271 renderers.add(font, tr);
1273 return tr;
1276 private Font getFont(Font font, boolean isBold, boolean isItalic)
1278 int fontStyle = isBold ? (isItalic ? Font.BOLD | Font.ITALIC : Font.BOLD)
1279 : (isItalic ? Font.ITALIC : Font.PLAIN);
1280 return font.deriveFont(fontStyle);
1283 // Update DrawState from html tag
1284 public TextRenderer update(String tag, boolean startStopRendering)
1286 DrawAttributes da = getDrawAttributes();
1287 boolean fontChanged = false;
1289 if(tag.compareToIgnoreCase("<b>") == 0)
1291 this.stack.add(new DrawAttributes(getFont(da.font, true, da.font.isItalic()), da.hyperlink, da.color));
1292 fontChanged = true;
1294 else if(tag.compareToIgnoreCase("</b>") == 0)
1296 if (this.stack.size() > 1)
1297 this.stack.remove(this.stack.size() - 1);
1298 fontChanged = true;
1300 else if(tag.compareToIgnoreCase("<i>") == 0)
1302 this.stack.add(new DrawAttributes(getFont(da.font, da.font.isBold(), true), da.hyperlink, da.color));
1303 fontChanged = true;
1305 else if(tag.compareToIgnoreCase("</i>") == 0)
1307 if (this.stack.size() > 1)
1308 this.stack.remove(this.stack.size() - 1);
1309 fontChanged = true;
1311 else if(tag.toLowerCase().startsWith("<a "))
1313 this.stack.add(new DrawAttributes(da.font, MultiLineTextRenderer.getAttributeFromTagHTML(tag, "href"), applyTextAlpha(linkColor)));
1314 if(startStopRendering)
1315 this.textRenderer.setColor(applyTextAlpha(linkColor));
1317 else if(tag.compareToIgnoreCase("</a>") == 0)
1319 if (this.stack.size() > 1)
1320 this.stack.remove(this.stack.size() - 1);
1321 if(startStopRendering)
1322 this.textRenderer.setColor(getDrawAttributes().color);
1324 else if(tag.toLowerCase().startsWith("<font "))
1326 String colorCode = MultiLineTextRenderer.getAttributeFromTagHTML(tag, "color");
1327 if (colorCode != null)
1329 Color color = da.color;
1332 color = applyTextAlpha(Color.decode(colorCode));
1334 catch (Exception e) {}
1335 this.stack.add(new DrawAttributes(da.font, da.hyperlink, color));
1336 if(startStopRendering)
1337 this.textRenderer.setColor(color);
1340 else if(tag.compareToIgnoreCase("</font>") == 0)
1342 if (this.stack.size() > 1)
1343 this.stack.remove(this.stack.size() - 1);
1344 if(startStopRendering)
1345 this.textRenderer.setColor(getDrawAttributes().color);
1348 if(fontChanged)
1350 // Terminate current rendering
1351 if(startStopRendering)
1352 this.textRenderer.end3DRendering();
1353 // Get new text renderer
1354 da = getDrawAttributes();
1355 this.textRenderer = getTextRenderer(da.font);
1356 // Resume rendering
1357 if(startStopRendering)
1359 this.textRenderer.begin3DRendering();
1360 this.textRenderer.setColor(da.color);
1364 return this.textRenderer;