/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ package org.opensourcephysics.display; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.geom.Rectangle2D; import java.util.Stack; import java.util.Vector; /* ************************************************************************** ** ** Class TextLine ** ************************************************************************** ** Copyright (C) 1996 Leigh Brookshaw ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. */ /** * TextLine is designed to bundle together all the information required * to draw short Strings with subscripts and superscripts. * * TextLine was modified by W. Christian to add Greek characters using TeX notation. * * @author Leigh Brookshaw */ public class TextLine { /** * Center the Text over the point */ public final static int CENTER = 0; /** * Position the Text to the Left of the point */ public final static int LEFT = 1; /** * Position the Text to the Right of the point */ public final static int RIGHT = 2; /** * Format to use when parsing a double */ public final static int SCIENTIFIC = 1; /** * Format to use when parsing a double */ public final static int ALGEBRAIC = 2; /* ** Minimum Point size allowed for script characters */ final static int MINIMUM_SIZE = 6; /* ********************** ** ** Protected Variables ** *********************/ /** * Decrease in size of successive script levels */ protected double script_fraction = 0.8; /** * Superscript offset */ protected double sup_offset = 0.6; /** * Subscript offset */ protected double sub_offset = 0.7; /** * Font to use for text */ protected Font font = null; /** * Text color */ protected Color color = null; /** * Background Color */ protected Color background = null; /** * The text to display */ protected String text = null; /** * The logical name of the font to use */ protected String fontname = "TimesRoman"; //$NON-NLS-1$ /** * The font size */ protected int fontsize = 0; /** * The font style */ protected int fontstyle = Font.PLAIN; /** * Text justification. Either CENTER, LEFT or RIGHT */ protected int justification = LEFT; /** * The width of the text using the current Font */ protected int width = 0; /** * The ascent using the current font */ protected int ascent = 0; /** * The maximum ascent using the current font */ protected int maxAscent = 0; /** * The descent using the current font */ protected int descent = 0; /** * The maximum descent using the current font */ protected int maxDescent = 0; /** * The height using the current font ie ascent+descent+leading */ protected int height = 0; /** * The leading using the current font */ protected int leading = 0; /** * Has the string been parsed! This only needs to be done once * unless the font is altered. */ protected boolean parse = true; /** * Local graphics context. */ protected Graphics lg = null; /** * The parsed string. Each element in the vector represents * a change of context in the string ie font change and offset. */ protected Vector<TextState> list = new Vector<TextState>(8, 4); /* ********************** ** ** Constructors ** *********************/ /** * Instantiate the class */ public TextLine() {} /** * Instantiate the class. * @param s String to parse. */ public TextLine(String s) { this.text = TeXParser.parseTeX(s); } /** * Instantiate the class * @param s String to parse. * @param f Font to use. */ public TextLine(String s, Font f) { this(s); font = f; if(font==null) { return; } fontname = f.getName(); fontstyle = f.getStyle(); fontsize = f.getSize(); } /** * Instantiate the class * @param s String to parse. * @param f Font to use. * @param c Color to use * @param j Justification */ public TextLine(String s, Font f, Color c, int j) { this(s, f); color = c; justification = j; } /** * Instantiate the class * @param s String to parse. * @param c Color to use */ public TextLine(String s, Color c) { this(s); color = c; } /** * Instantiate the class * @param f Font to use. * @param c Color to use * @param j Justification */ public TextLine(Font f, Color c, int j) { font = f; color = c; justification = j; if(font==null) { return; } fontname = f.getName(); fontstyle = f.getStyle(); fontsize = f.getSize(); } /* ***************** ** ** Public Methods ** *****************/ /** * Create a New Textline object copying the state of the existing * object into the new one. The state of the class is the color, * font, and justification ie everything but the string. */ public TextLine copyState() { return new TextLine(font, color, justification); } /** * Copy the state of the parsed Textline into the existing * object. * @param t The TextLine to get the state information from. */ public void copyState(TextLine t) { if(t==null) { return; } font = t.getFont(); color = t.getColor(); justification = t.getJustification(); if(font==null) { return; } fontname = font.getName(); fontstyle = font.getStyle(); fontsize = font.getSize(); parse = true; } /** * Set the Font to use with the class * @param f Font */ public void setFont(Font f) { if(f==null) { return; } font = f; fontname = f.getName(); fontstyle = f.getStyle(); fontsize = f.getSize(); parse = true; } /** * Set the String to use with the class * @param s String */ public void setText(String s) { text = TeXParser.parseTeX(s); parse = true; } /** * Set the Color to use with the class * @param c Color */ public void setColor(Color c) { color = c; } /** * Set the Background Color to use with the class * @param c Color */ public void setBackground(Color c) { background = c; } /** * Set the Justification to use with the class * @param i Justification */ public void setJustification(int i) { switch(i) { case CENTER : justification = CENTER; break; case LEFT : default : justification = LEFT; break; case RIGHT : justification = RIGHT; break; } } /** * @return the font the class is using */ public Font getFont() { return font; } /** * @return the String the class is using. */ public String getText() { return text; } /** * @return the Color the class is using. */ public Color getColor() { return color; } /** * @return the Background Color the class is using. */ public Color getBackground() { return background; } /** * @return the Justification the class is using. */ public int getJustification() { return justification; } /** * @param g Graphics context. * @return the Fontmetrics the class is using. */ public FontMetrics getFM(Graphics g) { if(g==null) { return null; } if(font==null) { return g.getFontMetrics(); } return g.getFontMetrics(font); } /** * @param g Graphics context. * @param ch The character. * @return the width of the character. */ public int charWidth(Graphics g, char ch) { FontMetrics fm; if(g==null) { return 0; } if(font==null) { fm = g.getFontMetrics(); } else { fm = g.getFontMetrics(font); } return fm.charWidth(ch); } /** * Returns the bounding box for this string. * @param g Graphics * @return Rectangle2D */ public Rectangle2D getStringBounds(Graphics g) { parseText(g); return new Rectangle2D.Float(0, 0, width, height); } /** * @param g Graphics context. * @return the width of the parsed text. */ public int getWidth(Graphics g) { parseText(g); return width; } /** * @param g Graphics context. * @return the height of the parsed text. */ public int getHeight(Graphics g) { parseText(g); return height; } /** * @param g Graphics context. * @return the ascent of the parsed text. */ public int getAscent(Graphics g) { if(g==null) { return 0; } parseText(g); return ascent; } /** * @param g Graphics context. * @return the maximum ascent of the parsed text. */ public int getMaxAscent(Graphics g) { if(g==null) { return 0; } parseText(g); return maxAscent; } /** * @param g Graphics context. * @return the descent of the parsed text. */ public int getDescent(Graphics g) { if(g==null) { return 0; } parseText(g); return descent; } /** * @param g Graphics context. * @return the maximum descent of the parsed text. */ public int getMaxDescent(Graphics g) { if(g==null) { return 0; } parseText(g); return maxDescent; } /** * @param g Graphics context. * @return the leading of the parsed text. */ public int getLeading(Graphics g) { if(g==null) { return 0; } parseText(g); return leading; } /** * parse the text. When the text is parsed the width, height, leading * are all calculated. The text will only be truly parsed if * the graphics context has changed or the text has changed or * the font has changed. Otherwise nothing is done when this * method is called. * @param g Graphics context. */ public void parseText(Graphics g) { TextState current = new TextState(); char ch; Stack<TextState> state = new Stack<TextState>(); int w = 0; if(lg!=g) { parse = true; } lg = g; if(!parse) { return; } parse = false; width = 0; leading = 0; ascent = 0; descent = 0; height = 0; maxAscent = 0; maxDescent = 0; if((text==null)||(g==null)) { return; } list.removeAllElements(); if(font==null) { current.f = g.getFont(); } else { current.f = font; } state.push(current); list.addElement(current); for(int i = 0; i<text.length(); i++) { ch = text.charAt(i); switch(ch) { case '$' : i++; if(i<text.length()) { current.s.append(text.charAt(i)); } break; /* ** Push the current state onto the state stack ** and start a new storage string */ case '{' : w = current.getWidth(g); if(!current.isEmpty()) { current = current.copyState(); list.addElement(current); } state.push(current); current.x += w; break; /* ** Pop the state off the state stack and set the current ** state to the top of the state stack */ case '}' : w = current.x+current.getWidth(g); state.pop(); current = state.peek().copyState(); list.addElement(current); current.x = w; break; case '^' : w = current.getWidth(g); if(!current.isEmpty()) { current = current.copyState(); list.addElement(current); } current.f = getScriptFont(current.f); current.x += w; current.y -= (int) ((current.getAscent(g))*sup_offset+0.5); break; case '_' : w = current.getWidth(g); if(!current.isEmpty()) { current = current.copyState(); list.addElement(current); } current.f = getScriptFont(current.f); current.x += w; current.y += (int) ((current.getDescent(g))*sub_offset+0.5); break; default : current.s.append(ch); break; } } Vector<TextState> vec; // added by W. Christian in case a parse is called during drawing. synchronized(list) { vec = new Vector<TextState>(list); } // added by W. Christian for(int i = 0; i<vec.size(); i++) { current = (vec.elementAt(i)); if(!current.isEmpty()) { width += current.getWidth(g); ascent = Math.max(ascent, Math.abs(current.y)+current.getAscent(g)); descent = Math.max(descent, Math.abs(current.y)+current.getDescent(g)); leading = Math.max(leading, current.getLeading(g)); maxDescent = Math.max(maxDescent, Math.abs(current.y)+current.getMaxDescent(g)); maxAscent = Math.max(maxAscent, Math.abs(current.y)+current.getMaxAscent(g)); } } height = ascent+descent+leading; return; } /** * @return true if the text has never been set or is null */ public boolean isNull() { return(text==null); } /** * Parse the text then draw it. * @param g Graphics context * @param x pixel position of the text * @param y pixel position of the text * @param j justification of the text */ public void drawText(Graphics g, int x, int y, int j) { justification = j; if(g==null) { return; } drawText(g, x, y); } /** * Parse the text then draw it without any rotation. * @param g Graphics context * @param x pixel position of the text * @param y pixel position of the text */ public void drawText(Graphics g, int x, int y) { TextState ts; int xoffset = x; int yoffset = y; if((g==null)||(text==null)) { return; } Graphics lg = g.create(); if(lg==null) { return; // added by W. Christian } parseText(g); if(justification==CENTER) { xoffset = x-width/2; } else if(justification==RIGHT) { xoffset = x-width; } if(background!=null) { lg.setColor(background); lg.fillRect(xoffset, yoffset-ascent, width, height); lg.setColor(g.getColor()); } if(font!=null) { lg.setFont(font); } if(color!=null) { lg.setColor(color); } Vector<TextState> vec; // added by W. Christian in case a parse is called during drawing. synchronized(list) { vec = new Vector<TextState>(list); } // added by W. Christian for(int i = 0; i<vec.size(); i++) { ts = (vec.elementAt(i)); if(ts.f!=null) { lg.setFont(ts.f); } if(ts.s!=null) { lg.drawString(ts.toString(), ts.x+xoffset, ts.y+yoffset); } } lg.dispose(); lg = null; } /** * @return Logical font name of the set font */ public String getFontName() { return fontname; } /** * @return Style of the set font */ public int getFontStyle() { return fontstyle; } /** * @return Size of the set font */ public int getFontSize() { return fontsize; } /** * Set the Logical font name of the current font * @param s Logical font name. */ public void setFontName(String s) { if(s==null) { return; } fontname = s; rebuildFont(); } /** * Set the Font style of the current font * @param i Font style. */ public void setFontStyle(int i) { fontstyle = i; rebuildFont(); } /** * Set the Font size of the current font * @param i Font size. */ public void setFontSize(int i) { fontsize = i; rebuildFont(); } /* ** Rebuild the font using the current fontname, fontstyle, and fontsize. */ private void rebuildFont() { parse = true; if((fontsize<=0)||(fontname==null)) { font = null; } else { font = new Font(fontname, fontstyle, fontsize); } } /** * @param f Font * @return The script font version of the parsed font using the * script_fraction variable. */ public Font getScriptFont(Font f) { int size; if(f==null) { return f; } size = f.getSize(); if(size<=MINIMUM_SIZE) { return f; } size = (int) ((f.getSize())*script_fraction+0.5); if(size<=MINIMUM_SIZE) { return f; } return new Font(f.getName(), f.getStyle(), size); } /** * Parse a double value. Precision is 6 figures, with 7 significant * figures. * @param d double to parse * return <i>true</i> if the parse was successful */ public boolean parseDouble(double d) { return parseDouble(d, 7, 6, ALGEBRAIC); } /** * Parse a double value. Number of significant figures is 1 greater than * the precision. * @param d double to parse * @param p precision of the number * return <i>true</i> if the parse was successful */ public boolean parseDouble(double d, int p) { return parseDouble(d, p+1, p, ALGEBRAIC); } /** * Parse a double value * @param d double to parse * @param n number of significant figures * @param p precision of the number * @param f format of the number scientific, algebraic etc. * return <i>true</i> if the parse was successful */ public boolean parseDouble(double d, int n, int p, int f) { double x = d; int left = n-p; double right = 0; int power; int exponent; int i; StringBuffer s = new StringBuffer(n+4); if(left<0) { System.out.println("TextLine.parseDouble: Precision > significant figures!"); //$NON-NLS-1$ return false; } if(d<0.0) { x = -d; s.append("-"); //$NON-NLS-1$ } // System.out.println("parseDouble: value = "+x); if(d==0.0) { exponent = 0; } else { exponent = (int) (Math.floor(TextLine.log10(x))); } // System.out.println("parseDouble: exponent = "+exponent); power = exponent-(left-1); // System.out.println("parseDouble: power = "+power); if(power<0) { for(i = power; i<0; i++) { x *= 10.0; } } else { for(i = 0; i<power; i++) { x /= 10.0; } } // System.out.println("parseDouble: adjusted value = "+x); left = (int) x; s.append(left); // System.out.println("parseDouble: left = "+left); if(p>0) { s.append('.'); right = x-left; for(i = 0; i<p; i++) { right *= 10; // if(i==p-1) right += 0.5; // s.append((int)(right)); // right -= (int)right; int digit = (int) Math.round(right); s.append(digit); right -= digit; } } // System.out.println("parseDouble: right = "+right); if(power!=0) { if(f==SCIENTIFIC) { s.append('E'); if(power<0) { s.append('-'); } else { s.append('+'); } power = Math.abs(power); if(power>9) { s.append(power); } else { s.append('0'); s.append(power); } } else { s.append("x10{^"); //$NON-NLS-1$ s.append(power); s.append("}"); //$NON-NLS-1$ } } setText(s.toString()); return true; } /** * @param x a double value * @return The log<sub>10</sub> */ static public double log10(double x) throws ArithmeticException { if(x<=0.0) { throw new ArithmeticException("range exception"); //$NON-NLS-1$ } return Math.log(x)/2.30258509299404568401; } } /** * A structure class used exclusively with the TextLine class. * When the Text changes state (new font, new color, new offset) * then this class holds the information plus the substring * that the state pertains to. */ class TextState extends Object { Font f = null; StringBuffer s = null; int x = 0; int y = 0; /** * Constructor TextState */ public TextState() { s = new StringBuffer(); } public TextState copyAll() { TextState tmp = copyState(); if(s.length()==0) { return tmp; } for(int i = 0; i<s.length(); i++) { tmp.s.append(s.charAt(i)); } return tmp; } public TextState copyState() { TextState tmp = new TextState(); tmp.f = f; tmp.x = x; tmp.y = y; return tmp; } public String toString() { return s.toString(); } public boolean isEmpty() { return(s.length()==0); } public int getWidth(Graphics g) { if((g==null)||(f==null)||(s.length()==0)) { return 0; } return g.getFontMetrics(f).stringWidth(s.toString()); } public int getHeight(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getHeight(); } public int getAscent(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getAscent(); } public int getDescent(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getDescent(); } public int getMaxAscent(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getMaxAscent(); } public int getMaxDescent(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getMaxDescent(); } public int getLeading(Graphics g) { if((g==null)||(f==null)) { return 0; } return g.getFontMetrics(f).getLeading(); } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */