2 Copyright (C) 2001, 2006, 2007 United States Government
3 as represented by the Administrator of the
4 National Aeronautics and Space Administration.
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
;
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
;
25 * Multi line, rectangle bound text renderer with (very) minimal html support.
27 * The {@link MultiLineTextRenderer} (MLTR) handles wrapping, measuring and drawing
28 * of multiline text strings using Sun's JOGL {@link TextRenderer}.
31 * A multiline text string is a character string containing new line characters
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.
39 * <p><b>Usage:</b></p>
41 * <p>Instantiation:</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.
48 * Font font = Font.decode("Arial-PLAIN-12");
49 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(font);
53 * TextRenderer tr = new TextRenderer(Font.decode("Arial-PLAIN-10"));
54 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(tr);
57 * <p>Drawing regular text:</p>
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();
71 * <p>Wrapping text to fit inside a width and optionaly a height</p>
73 * The MLTR wrap method will insert new line characters inside the text so that
74 * it fits a given width in pixels.
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();
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));
89 * <p>Measuring text</p>
91 * Rectangle2D textBounds = mltr.getBounds(text);
94 * The textBounds rectangle returned contains the width and height of the text
95 * as it would be drawn with the current font.
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);
105 * <p><b>HTML support</b></p>
107 * Supported tags are:
109 * <li><p></p>, <br> <br /></li>
110 * <li><b></b></li>
111 * <li><i></i></li>
112 * <li><a href="..."></a></li>
113 * <li><font color="#ffffff"></font></li>
121 * See {@link AbstractAnnotation}.drawAnnotation() for more usage details.
124 * @author: Patrick Murris
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
)
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
)
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
)
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
)
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
)
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
);
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
++)
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());
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
++)
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());
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(' ');
451 String source
= text
.trim();
453 int end
= source
.indexOf(' ', start
+ 1);
454 while(start
< source
.length())
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
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();
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');
527 heightExceeded
= true;
530 if(i
< lines
.length
- 1 && !heightExceeded
)
531 wrappedText
.append('\n');
533 // Add continuation string if text truncated
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();
555 int end
= source
.indexOf(' ', start
+ 1);
556 while(start
< source
.length())
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
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)
581 // Line is empty, force at least one word
582 line
.append(word
.trim());
585 // Move forward in source string
587 if(start
< source
.length() - 1)
589 end
= source
.indexOf(' ', start
+ 1);
593 wrappedText
.append(line
);
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
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
);
658 return matcher
.group(1);
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
,
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
);
698 String
[] lines
= text
.split("\n");
699 StringBuffer linePart
= new StringBuffer();
700 for(int i
= 0; i
< lines
.length
; i
++)
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
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
);
776 DrawState ds
= new DrawState(renderers
, this.textRenderer
.getFont(), null, this.textColor
);
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
++)
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());
795 drawY
-= lineBounds
.getHeight();
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
812 // Process current line part and draw
813 linePart
.append(lines
[i
].substring(start
, matcher
.start()));
814 if(linePart
.length() > 0)
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
));
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
);
865 DrawState ds
= new DrawState(renderers
, this.textRenderer
.getFont(), null, this.textColor
);
870 String
[] lines
= text
.split("\n");
871 StringBuffer linePart
= new StringBuffer();
872 for(int i
= 0; i
< lines
.length
; i
++)
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());
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
);
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
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
);
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
);
984 DrawState ds
= new DrawState(renderers
, this.textRenderer
.getFont(), null, this.textColor
);
985 int width
= (int)dimension
.getWidth();
986 int height
= (int)dimension
.getHeight();
989 String
[] lines
= text
.split("\n");
990 StringBuffer wrappedText
= new StringBuffer();
991 int currentHeight
= 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;
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
1020 word
= source
.substring(wordStart
, matcher
.start());
1021 if(lineWidth
+ wordWidth
<= width
)
1023 // Keep adding to the current line
1025 lineWidth
+= wordWidth
;
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
;
1042 line
.delete(0, line
.length());
1044 lineWidth
= wordWidth
;
1045 // Keep track of max line height
1046 maxLineHeight
= getMaxLineHeight(ds
.textRenderer
);
1050 heightExceeded
= true;
1055 // Line is empty, force at least one word
1057 lineWidth
= wordWidth
;
1060 // Move on from space found
1061 start
= matcher
.start();
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
)
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
1099 word
= source
.substring(wordStart
);
1100 if(lineWidth
+ wordWidth
<= width
)
1102 // Keep adding to the current line
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
;
1118 line
.delete(0, line
.length());
1120 // Keep track of max line height
1121 maxLineHeight
= getMaxLineHeight(ds
.textRenderer
);
1125 heightExceeded
= true;
1130 // Line is empty, force at least one word
1134 if(height
<= 0 || currentHeight
+ maxLineHeight
<= height
)
1136 wrappedText
.append(line
);
1137 currentHeight
+= maxLineHeight
+ this.lineSpacing
;
1142 heightExceeded
= true;
1148 // line doesnt need to be wrapped
1149 if(height
<= 0 || currentHeight
+ maxLineHeight
<= height
)
1151 wrappedText
.append(source
);
1152 currentHeight
+= maxLineHeight
+ this.lineSpacing
;
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
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
)
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
)
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
)
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);
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
)
1235 this.hyperlink
= hyperlink
;
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)
1262 return this.stack
.get(this.stack
.size() - 1);
1265 private TextRenderer
getTextRenderer(Font font
)
1267 TextRenderer tr
= this.renderers
.get(font
);
1270 tr
= new TextRenderer(font
, true, true);
1271 renderers
.add(font
, 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
));
1294 else if(tag
.compareToIgnoreCase("</b>") == 0)
1296 if (this.stack
.size() > 1)
1297 this.stack
.remove(this.stack
.size() - 1);
1300 else if(tag
.compareToIgnoreCase("<i>") == 0)
1302 this.stack
.add(new DrawAttributes(getFont(da
.font
, da
.font
.isBold(), true), da
.hyperlink
, da
.color
));
1305 else if(tag
.compareToIgnoreCase("</i>") == 0)
1307 if (this.stack
.size() > 1)
1308 this.stack
.remove(this.stack
.size() - 1);
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
);
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
);
1357 if(startStopRendering
)
1359 this.textRenderer
.begin3DRendering();
1360 this.textRenderer
.setColor(da
.color
);
1364 return this.textRenderer
;