All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.render.MultiLineTextRenderer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */
package gov.nasa.worldwind.render;

import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.util.*;

import com.jogamp.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. 

The {@link MultiLineTextRenderer} * (MLTR) handles wrapping, measuring and drawing of multiline text strings using Sun's JOGL {@link TextRenderer}.

*

A multiline text string is a character string containing new line characters in between lines.

MLTR can * handle both regular text with new line separators and a very minimal implementation of HTML. Each type of text has * its own methods though.

Usage:

Instantiation:

The MLTR needs a Font or a * TextRenderer to be instantiated. 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.

*
 * Font font = Font.decode("Arial-PLAIN-12");
 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(font);
 * 
* or *
 * TextRenderer tr = new TextRenderer(Font.decode("Arial-PLAIN-10"));
 * MultiLineTextRenderer mltr = new MultiLineTextRenderer(tr);
 * 
*

Drawing regular text:

*
 * 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(); *

*

Wrapping text to fit inside a width and optionally a height

The MLTR wrap method will insert new line * characters inside the text so that it fits a given width in pixels.

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();

*
 * // 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)); *

*

Measuring text

*
 * Rectangle2D textBounds = mltr.getBounds(text);
 * 
*

The textBounds rectangle returned contains the width and height of the text as it would be drawn with the current * font.

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); ...

HTML support

Supported tags are:

    *
  • <p></p>, <br> <br />
  • <b></b>
  • <i></i>
  • *
  • <a href="..."></a>
  • <font color="#ffffff"></font>

...

*

See {@link AbstractAnnotation}.drawAnnotation() for more usage details.

* * @author Patrick Murris * @version $Id: MultiLineTextRenderer.java 2053 2014-06-10 20:16:57Z tgaskins $ */ public class MultiLineTextRenderer { protected TextRenderer textRenderer; protected int lineSpacing = 0; // Inter line spacing in pixels protected int lineHeight = 14; // Will be set by getBounds() or by application protected String textAlign = AVKey.LEFT; // Text alignment protected String continuationString = "..."; protected Color textColor = Color.DARK_GRAY; protected Color backColor = Color.LIGHT_GRAY; protected Color linkColor = Color.BLUE; // HTML Picking protected boolean isPicking = false; protected DrawContext drawContext; protected PickSupport pickSupport; protected Object pickObject; protected Position pickPosition; 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, false); 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 AVKey#LEFT} the default, {@link AVKey#CENTER} or {@link * AVKey#RIGHT}. * * @return the current text alignment. */ public String getTextAlign() { return this.textAlign; } /** * Set the current text alignment. Can be one of {@link AVKey#LEFT} the default, {@link AVKey#CENTER} or {@link * AVKey#RIGHT}. * * @param align the current text alignment. */ public void setTextAlign(String align) { if (!align.equals(AVKey.LEFT) && !align.equals(AVKey.CENTER) && !align.equals(AVKey.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 (String line : lines) { Rectangle2D lineBounds = this.textRenderer.getBounds(line); 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 EFFECT_NONE, * EFFECT_SHADOW or EFFECT_OUTLINE. */ 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 EFFECT_NONE, * EFFECT_SHADOW or EFFECT_OUTLINE. */ 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(AVKey.TEXT_EFFECT_SHADOW)) { this.textRenderer.setColor(backColor); this.draw(text, x + 1, y - 1, textLineHeight); this.textRenderer.setColor(textColor); } else if (effect.equals(AVKey.TEXT_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 (String line : lines) { int xAligned = x; if (this.textAlign.equals(AVKey.CENTER)) xAligned = x - (int) (this.textRenderer.getBounds(line).getWidth() / 2); else if (this.textAlign.equals(AVKey.RIGHT)) xAligned = x - (int) (this.textRenderer.getBounds(line).getWidth()); y -= textLineHeight; this.textRenderer.draw3D(line, 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 (String line : lines) { int xAligned = x; if (this.textAlign.equals(AVKey.CENTER)) xAligned = x - (int) (this.textRenderer.getBounds(line).getWidth() / 2); else if (this.textAlign.equals(AVKey.RIGHT)) xAligned = x - (int) (this.textRenderer.getBounds(line).getWidth()); y -= textLineHeight; drawLineWithUniqueColors(line, xAligned, y, dc, pickSupport, refObject, refPosition); y -= this.lineSpacing; } } protected void drawLineWithUniqueColors(String text, int x, int y, DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. //float spaceWidth = this.textRenderer.getCharWidth(' '); float drawX; 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)); 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 gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); drawFilledRectangle(dc, drawX + wordBounds.getX(), y - 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 width the maximum width in pixels the text can occupy. * @param height if not zero, the maximum height in pixels the text can occupy. * * @return the wrapped string. */ public String wrap(String text, int width, int height) { if (text == null) { String msg = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } 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 protected 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 doesn't need to be wrapped wrappedText.append(source); } return wrappedText.toString(); } //** Very very simple html support ******************************************* // Handles

,
or
, , , // and . //**************************************************************************** protected static Pattern SGMLPattern = Pattern.compile("<[^\\s].*?>"); // Find sgml tags protected static Pattern SGMLOrSpacePattern = Pattern.compile("(<[^\\s].*?>)|(\\s)"); // Find sgml tags or spaces /** * 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); } Matcher matcher = SGMLPattern.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)", "\n"); // Replace
with one new line text = text.replaceAll("(?i)", ""); // Replace

with nothing text = text.replaceAll("(?i)

", "\n\n"); // Replace

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; } // --- HTML Word iterator based methods -------------------------------- /** * Returns the bounding rectangle for a multi-line html string. * * @param text the multi-line html text to evaluate. * @param renderers a {@link TextRendererCache} instance. * * @return the bounding rectangle for the rendered text. */ public Rectangle 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 getTextBoundsHTML(text, ds).getBounds(); } protected Rectangle2D getTextBoundsHTML(String text, DrawState ds) { double width = 0; double height = 0; String[] lines = text.split("\n"); for (String line : lines) { Rectangle2D lineBounds = getLineBoundsHTML(line, ds); width = Math.max(lineBounds.getWidth(), width); height += lineBounds.getHeight() + this.lineSpacing; } height -= this.lineSpacing; // remove last line spacing return new Rectangle2D.Double(0, 0, width, height); } protected Rectangle2D getLineBoundsHTML(String line, DrawState ds) { double width = 0; double height = getMaxLineHeight(ds.textRenderer); Iterator wi = new WordIteratorHTML(line); while (wi.hasNext()) { // Acumulate words dimensions Rectangle2D wordBounds = getWordBoundsHTML((String) wi.next(), ds); width += wordBounds.getWidth(); height = Math.max(wordBounds.getHeight(), height); // Count a space between words - not after last. if (wi.hasNext()) width += ds.textRenderer.getCharWidth(' '); } return new Rectangle2D.Double(0, 0, width, height); } protected Rectangle2D getWordBoundsHTML(String word, DrawState ds) { double width = 0; double height = getMaxLineHeight(ds.textRenderer); int start = 0; String part; Rectangle2D partBounds; Matcher matcher = SGMLOrSpacePattern.matcher(word); // html tags or spaces while (matcher.find()) { if (!matcher.group().equals(" ")) // html tag - not a space { if (matcher.start() > start) { // Measure part part = word.substring(start, matcher.start()); partBounds = ds.textRenderer.getBounds(part); width += partBounds.getWidth() + partBounds.getX(); } // Apply html tag to draw state ds.updateFromHTMLTag(matcher.group(), false); height = Math.max(getMaxLineHeight(ds.textRenderer), height); start = matcher.end(); } } if (start < word.length()) { // Measure remaining part if any part = word.substring(start, word.length()); partBounds = ds.textRenderer.getBounds(part); width += partBounds.getWidth() + partBounds.getX(); } return new Rectangle2D.Double(0, 0, width, height); } /** * 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 width the maximum width in pixels one text line can occupy. * @param height if not zero, the maximum height the text can occupy. * @param renderers a {@link TextRendererCache} instance. * * @return the wrapped html string */ public String wrapHTML(String text, double width, double height, 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 wrapTextHTML(text, width, height, ds); } protected String wrapTextHTML(String text, double width, double height, DrawState ds) { // Save passed draw state in case we need to trim text later DrawState savedState = new DrawState(ds); StringBuffer wrappedText = new StringBuffer(); String[] lines = text.split("\n"); for (String line : lines) { wrappedText.append(wrappedText.length() > 0 ? "\n" : ""); wrappedText.append(wrapLineHTML(line, width, ds)); } if (height > 0) return trimTextHTML(wrappedText.toString(), height, savedState); return wrappedText.toString(); } protected String trimTextHTML(String text, double height, DrawState ds) { StringBuffer wrappedText = new StringBuffer(); double currentHeight = 0; String[] lines = text.split("\n"); for (String line : lines) { Rectangle2D lineBounds = getLineBoundsHTML(line, ds); if (currentHeight + lineBounds.getHeight() <= height) { wrappedText.append(wrappedText.length() > 0 ? "\n" : ""); wrappedText.append(line); currentHeight += lineBounds.getHeight() + this.lineSpacing; } else { // Text is longer then allowed. Truncate and add continuation string wrappedText.append(this.continuationString); break; } } return wrappedText.toString(); } protected String wrapLineHTML(String line, double width, DrawState ds) { // Save passed draw state in case we need to wrap DrawState savedState = new DrawState(ds); // Measure line - note this updates the caller draw state Rectangle2D lineBounds = getLineBoundsHTML(line, ds); if (lineBounds.getWidth() <= width) return line; // The line needs to be wrapped double spaceWidth, wordWidth, lineWidth = 0; StringBuffer wrappedText = new StringBuffer(); WordIteratorHTML wi = new WordIteratorHTML(line); while (wi.hasNext()) { String word = wi.next(); spaceWidth = savedState.textRenderer.getCharWidth(' '); wordWidth = getWordBoundsHTML(word, savedState).getWidth(); if (lineWidth == 0 || lineWidth + wordWidth + (lineWidth > 0 ? spaceWidth : 0) <= width) { // Add space and word to line wrappedText.append(lineWidth > 0 ? " " : ""); wrappedText.append(word); lineWidth += wordWidth + (lineWidth > 0 ? spaceWidth : 0); } else { // Width exceeded, start new line wrappedText.append("\n"); wrappedText.append(word); lineWidth = wordWidth; } } return wrappedText.toString(); } /** * 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 {@link TextRendererCache} instance. * @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 (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); } // Set picking this.drawContext = dc; this.pickSupport = pickSupport; this.pickObject = refObject; this.pickPosition = refPosition; this.isPicking = true; // Draw try { drawHTML(text, x, y, renderers); } finally { this.isPicking = false; } } /** * 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 {@link TextRendererCache} instance. */ public void drawHTML(String text, double x, double 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); } // Draw attributes DrawState ds = new DrawState(renderers, this.textRenderer.getFont(), null, this.textColor); drawTextHTML(text, x, y, ds); } protected void drawTextHTML(String text, double x, double y, DrawState ds) { if (!this.isPicking) ds.textRenderer.begin3DRendering(); try { ds.textRenderer.setColor(this.textColor); Rectangle2D lineBounds; double drawX, drawY = y; String[] lines = text.split("\n"); for (String line : lines) { // Set line start x drawX = x; lineBounds = getTextBoundsHTML(line, new DrawState(ds)); if (this.textAlign.equals(AVKey.CENTER)) drawX = x - lineBounds.getWidth() / 2; else if (this.textAlign.equals(AVKey.RIGHT)) drawX = x - lineBounds.getWidth(); // Skip line height drawY -= lineBounds.getHeight(); // Draw line drawLineHTML(line, drawX, drawY, ds); // Skip line spacing drawY -= this.lineSpacing; } } finally { if (!this.isPicking) ds.textRenderer.end3DRendering(); } } protected void drawLineHTML(String line, double x, double y, DrawState ds) { String word; Rectangle2D wordBounds; WordIteratorHTML wi = new WordIteratorHTML(line); double drawX = x; while (wi.hasNext()) { word = wi.next(); wordBounds = getWordBoundsHTML(word, new DrawState(ds)); if (this.isPicking) pickWordHTML(word, drawX, y, ds); else drawWordHTML(word, drawX, y, ds); drawX += wordBounds.getWidth() + ds.textRenderer.getCharWidth(' '); } } protected void drawWordHTML(String word, double x, double y, DrawState ds) { double drawX = x; int start = 0; String part; Rectangle2D partBounds; Matcher matcher = SGMLOrSpacePattern.matcher(word); // html tags or spaces while (matcher.find()) { if (!matcher.group().equals(" ")) // html tag - not a space { if (matcher.start() > start) { // Draw part part = word.substring(start, matcher.start()); partBounds = ds.textRenderer.getBounds(part); ds.textRenderer.draw(part, (int) drawX, (int) y); drawX += partBounds.getWidth() + partBounds.getX(); } // Apply html tag to draw state ds.updateFromHTMLTag(matcher.group(), true); start = matcher.end(); } } if (start < word.length()) { // Draw remaining part if any part = word.substring(start, word.length()); ds.textRenderer.draw(part, (int) drawX, (int) y); } } protected void pickWordHTML(String word, double x, double y, DrawState ds) { double drawX = x; int start = 0; String part; Rectangle2D partBounds; boolean expandStart = true; Matcher matcher = SGMLOrSpacePattern.matcher(word); // html tags or spaces while (matcher.find()) { if (!matcher.group().equals(" ")) // html tag - not a space { if (matcher.start() > start) { // Pick part part = word.substring(start, matcher.start()); partBounds = ds.textRenderer.getBounds(part); pickWordPartHTML(part, drawX, y, partBounds, ds, expandStart); expandStart = false; drawX += partBounds.getWidth() + partBounds.getX(); } // Apply html tag to draw state ds.updateFromHTMLTag(matcher.group(), false); start = matcher.end(); } } if (start < word.length()) { // Pick remaining part if any part = word.substring(start, word.length()); partBounds = ds.textRenderer.getBounds(part); pickWordPartHTML(part, drawX, y, partBounds, ds, expandStart); } } protected void pickWordPartHTML(String word, double x, double y, Rectangle2D partBounds, DrawState ds, boolean expandStart) { String hyperlink = ds.getDrawAttributes().hyperlink; // Extend pick rectangle width to fill a bit more then half a space before and after the word. // Extend height a little too. double spaceWidth = ds.textRenderer.getCharWidth(' ') * 1.5; double height = this.getMaxLineHeight(ds.textRenderer); Rectangle2D pickBounds; if (expandStart) { pickBounds = new Rectangle2D.Double(0, 0, partBounds.getWidth() + partBounds.getX() + spaceWidth, height * 1.1); x -= spaceWidth / 2; } else { pickBounds = new Rectangle2D.Double(0, 0, partBounds.getWidth() + partBounds.getX() + spaceWidth / 2, height * 1.1); } pickWord(word, hyperlink, x, y, pickBounds, this.drawContext, this.pickSupport, this.pickObject, this.pickPosition); } protected void pickWord(String word, String hyperlink, double drawX, double drawY, Rectangle2D wordBounds, DrawContext dc, PickSupport pickSupport, Object refObject, Position refPosition) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. // 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 gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); drawFilledRectangle(dc, drawX, drawY - wordBounds.getHeight() / 4, // TODO: handle font descent properly wordBounds.getWidth(), wordBounds.getHeight()); } // Draw a filled rectangle protected void drawFilledRectangle(DrawContext dc, double x, double y, double width, double height) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. gl.glBegin(GL2.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(); } // -- HTML word iterator ------------------------------------ /** * Iterates through words from an HTML text line. Note that returned 'words' can contain html tags at any place, * before, inside or after the word. */ public static class WordIteratorHTML implements Iterator { protected ArrayList words; protected int nextWord = -1; protected static Pattern SGMLOrSpacePattern = Pattern.compile("(<[^\\s].*?>)|(\\s)"); public WordIteratorHTML(String text) { Matcher matcher = SGMLOrSpacePattern.matcher(text); this.words = new ArrayList(); int start = 0; while (matcher.find()) { if (matcher.group().equals(" ")) { // Space found, add word to list addWord(text.substring(start, matcher.end())); start = matcher.end(); // move after space found } } // Add end of text if any if (start < text.length()) addWord(text.substring(start)); // Set next word index if (this.words.size() > 0) this.nextWord = 0; } protected void addWord(String word) { word = word.trim(); if (word.length() > 0) words.add(word); } public boolean hasNext() { return this.nextWord != -1 && this.nextWord < words.size(); } public String next() { return words.get(this.nextWord++); } public void remove() { } } // -- HTML Draw state handling ----------------------------------- protected class DrawState { protected class DrawAttributes { protected final Font font; protected String hyperlink; protected final Color color; public DrawAttributes(Font font, String hyperlink, Color color) { this.font = font; this.hyperlink = hyperlink; this.color = color; } } protected ArrayList stack = new ArrayList(); protected TextRendererCache renderers; public TextRenderer textRenderer; protected Pattern SGMLPattern = Pattern.compile("(<[^\\s].*?>)"); public DrawState(TextRendererCache renderers, Font font, String hyperlink, Color color) { this.push(new DrawAttributes(font, hyperlink, color)); this.renderers = renderers; this.textRenderer = getTextRenderer(font); } public DrawState(DrawState ds) { for (DrawAttributes da : ds.stack) { this.push(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); } protected TextRenderer getTextRenderer(Font font) { return OGLTextRenderer.getOrCreateTextRenderer(this.renderers, font); } protected 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 text public void updateFromHTMLText(String text, boolean startStopRendering) { Matcher matcher = SGMLPattern.matcher(text); while (matcher.find()) { updateFromHTMLTag(matcher.group(), startStopRendering); } } // Update DrawState from html tag public void updateFromHTMLTag(String tag, boolean startStopRendering) { DrawAttributes da = getDrawAttributes(); boolean fontChanged = false; if (tag.equalsIgnoreCase("")) { this.push(new DrawAttributes(getFont(da.font, true, da.font.isItalic()), da.hyperlink, da.color)); fontChanged = true; } else if (tag.equalsIgnoreCase("")) { this.pop(); fontChanged = true; } else if (tag.equalsIgnoreCase("")) { this.push(new DrawAttributes(getFont(da.font, da.font.isBold(), true), da.hyperlink, da.color)); fontChanged = true; } else if (tag.equalsIgnoreCase("")) { this.pop(); fontChanged = true; } else if (tag.toLowerCase().startsWith("")) { this.pop(); if (startStopRendering) this.textRenderer.setColor(getDrawAttributes().color); } else if (tag.toLowerCase().startsWith("")) { this.pop(); 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); } } } protected void push(DrawAttributes da) { this.stack.add(da); } protected void pop() { if (this.stack.size() > 1) this.stack.remove(this.stack.size() - 1); } protected Color applyTextAlpha(Color color) { return new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() * textColor.getAlpha() / 255); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy