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