-1-
【手書認識・グラフ描画
Step3】認識した数式をもとに関数グラフを描画する
<数式の構造解析> 一般に、”1+3”などと文字列で数式をうけとっても、コンピュータはそれをそのままで式とは認識しな い。あくまで、文字列は文字の並びであり、そこに数学的な意味は含まれない。数式として計算するた めには、プログラムによって数式の構造を解析し、コンピュータが計算できる形式に変換する必要があ る。 今回のプログラムでは、通常の記法でかかれた数式の文字列を、逆ポーランド記法(Reverse Polish Notation, RPN)という数式の書き方に変換して計算している。これは、数字などを演算子の後に記述す る書き方で、コンピュータとの親和性が高い。 本演習では、数式解析してRPN に変換し、計算をする部分はソースごと提供するので、使い方のみ説 明する。たとえば x2+ 1 x + 3 という式を計算したい場合、関数に渡すために数式を次のように文字列で表現する。 String str = “x^2+1/(x+3)”; この式で、x=5 の値を求めたい場合はTerm term = RPNUtil.RPN(str); double y = term.func(5.0);
とすると、y に解 25.125 が代入される。ただし、Term クラスと RPNUtil クラスは Java が提供するも
のではなく、rpn パッケージに含まれる独自のクラスである。
<GeneralPath について>
関数をグラフ描画するにあたって、矢印を描画する必要がある。今回は、一般的な直線や曲線の集合 をあらわすGeneralPath というクラスを使って描画する。GeneralPath は線の集合であり、それらの線 が必ずしもつながっている必要はない。一般に、点(x1, y1)から点(x2, y2)に直線を引くには
GeneralPath path = new GeneralPath(); path.moveTo(x1, y1); // x1, y1 に移動
path.lineTo(x2, y2); // x2, y2 まで直線をひく g2d.draw(path); // 描画
-2- のようにする。
上の例では、GeneralPath を new するときに、コンストラクタに何も指定していないが、直線を表す
Line, 四角形を表す Rectangle などを引数に与えてもよい。 ※矢印を作成するメソッドの引数
矢印を表すGeneralPath を生成するために、getArrowPath(point1, point2, barb, degree)というメソ ッドを定義して使っている。引数の意味は、次の図にあるとおりである。
※Point2D.Float クラス
getArrowPath メソッドの引数 point1, point2 は ava.awt.geom パッケージの Point2D.Float クラスで ある。このクラスは、Point2D というクラスの中に宣言されたサブクラスで、他に Point2D.Double クラ スもある。宣言方法は次のようになっている。
public abstract class Point2D implements Cloneable {
public static class Float extends Point2D implements Serializable { public float x;
public float y; ・・・(中略)・・・ }
public static class Double extends Point2D implements Serializable { public double x; public double y; ・・・(中略)・・・ } ・・・(中略)・・・ } このように入れ子になってるクラスのことを内部クラス、またはインナークラスと呼ぶ。今まで使って いたjava.awt パッケージの Point クラスは、x 座標と y 座標が int 型であったが、Point2D.Float クラス はfloat 型、Point2D.Double クラスは double 型である。
barb
point1
point2 degree
-3- <作成手順> 1.rpn.zip を適当な場所で解凍し、rpn フォルダをコピーしてプロジェクトの src フォルダ (D:¥workspace¥Graph¥src)の中に貼り付ける。 2.パッケージ・エクスプローラでGraph プロジェクトを選択し、右クリック→リフレッシュ rpn パッケージがあることを確認 3.GraphPanel.java のソースを編集 4.GraphFrame.java でインポート文を2つ追加し、グラフ描画メソッドを編集 import javax.swing.JOptionPane; import rpn.RPNException;
public class GraphFrame extends javax.swing.JFrame { ・・・(中略)・・・
private void graphButtonActionPerformed(ActionEvent evt) { try { // 範囲取得 graphPanel.setxMin(Double.parseDouble(xMinField.getText())); graphPanel.setxMax(Double.parseDouble(xMaxField.getText())); graphPanel.setyMin(Double.parseDouble(yMinField.getText())); graphPanel.setyMax(Double.parseDouble(yMaxField.getText())); // 数式設定 graphPanel.setFormula(paintPanel.getFormula()); graphPanel.repaint(); } catch (RPNException e) { JOptionPane.showMessageDialog(this, "正しい数式を設定してください\n"+e.getMessage()); } catch (NumberFormatException fe) {
JOptionPane.showMessageDialog(this, "範囲を正しく入力してください"); } } } 5.実行し、適当な式を 1 文字ずつ入力・認識し、グラフ描画ボタンを押す。グラフが正しく描画され ることを確認する。
GraphPanel.java -4- package graph; 1 2 import java.awt.Color; 3 import java.awt.Graphics; 4 import java.awt.Graphics2D; 5 import java.awt.Point; 6 import java.awt.font.TextAttribute; 7 import java.awt.geom.GeneralPath; 8 import java.awt.geom.Line2D; 9 import java.awt.geom.Point2D; 10 import java.text.AttributedCharacterIterator; 11 import java.text.AttributedString; 12 import java.text.CharacterIterator; 13 14 import javax.swing.JPanel; 15 16 import rpn.RPNException; 17 import rpn.RPNUtil; 18 import rpn.Term; 19 20
public class GraphPanel extends JPanel { 21
22
private Term term; // 数式オブジェクト 23
private double xMin=-10, xMax=10; // x の表示範囲(関数の座標) 24
private double yMin=-10, yMax=10; // y の表示範囲(関数の座標) 25
private int divNum = 200; // グラフ線の刻み幅 26 27 /** 28 * コンストラクタ(何もしない) 29 */ 30 public GraphPanel() { 31 } 32 33 /** 34 * 描画メソッド 35 */ 36 @Override 37
public void paintComponent(Graphics g) { 38 super.paintComponent(g); 39 Graphics2D g2d = (Graphics2D)g; 40 // 白色背景 41 g2d.setColor(Color.WHITE); 42
g2d.fillRect(0, 0, getWidth(), getHeight()); 43 // グラフ描画 44 if (term != null) { 45 // 座標軸 --- 46 // 黒色設定 47 g2d.setColor(Color.BLACK); 48 // 原点を取得 49
Point origin = getGraphicalCoordinate(0, 0); 50 // x 軸の矢印の始点と終点を取得 51 /* getArrowPath で使うので Point2D.Float にする。 52 * これは座標を float で保持する点のクラス */ 53
Point2D.Float pf1 = new Point2D.Float(0, origin.y); 54
Point2D.Float pf2 = new Point2D.Float(getWidth(), origin.y); 55
// x 軸の矢印を取得 56
GeneralPath arrow = getArrowPath(pf1, pf2, 10, 20); 57 // x 軸の矢印を描画 58 g2d.draw(arrow); 59 // y 軸の矢印の始点と終点を取得 60
pf1 = new Point2D.Float(origin.x, getHeight()); 61 pf2 = new Point2D.Float(origin.x, 0); 62 // y 軸の矢印を取得 63 arrow = getArrowPath(pf1, pf2, 10, 20); 64
GraphPanel.java -5- // y 軸の矢印を描画 65 g2d.draw(arrow); 66 67 // 関数線 --- 68 // 赤色設定 69 g2d.setColor(Color.RED); 70 double x1,y1,x2,y2; 71 Point p1, p2; 72 // divNum 個の刻みで関数線を描画 73
for (int i=0; i<divNum; i++) { 74 // i 番目の点を計算 75 // ※数式オブジェクト term.func(x 座標)で y 座標を取得可能 76 x1 = xMin + (xMax-xMin)*i/(double)divNum; 77 y1 = term.func(x1); 78 // i+1 番目の点を計算 79 x2 = xMin + (xMax-xMin)*(i+1)/(double)divNum; 80 y2 = term.func(x2); 81 // 2 つの点の描画すべき点を取得 82 p1 = getGraphicalCoordinate(x1, y1); 83 p2 = getGraphicalCoordinate(x2, y2); 84 // パネルの中だったら描画する 85
if (p1.y >= 0 && p1.y <= getHeight() 86
&& p2.y >= 0 && p2.y <= getHeight()) 87
// 直線を描画 88
g2d.drawLine(p1.x, p1.y, p2.x, p2.y); 89 } 90 } 91 } 92 93 /** 94 * 関数の座標系からパネルの座標を取得 95 * @param x 96 * @param y 97 * @return パネル上の点 98 */ 99
private Point getGraphicalCoordinate(double x, double y) { 100
return new Point((int)(getWidth()*(x-xMin)/(xMax-xMin)), 101 (int)(getHeight() - getHeight()*(y-yMin)/(yMax-yMin))); 102 } 103 104 /** 105 * パスによって矢印線を表すオブジェクトを取得<br> 106 * point2 に矢印をつける 107 * @param point1 始点 108 * @param point2 終点 109 * @param barb 矢印部分の長さ 110
* @param degree 矢印部分の角度(degree) 111
* @return 矢印を表すパス 112
*/ 113
private GeneralPath getArrowPath(Point2D.Float point1, Point2D.Float point2, int barb, int 114
degree) { 115
// 角度をラジアンにする 116
double phi = Math.toRadians(degree); 117
// 始点と終点によって決まる線分の角度を求める 118
double theta = Math.atan2(point2.y - point1.y, point2.x - point1.x); 119
// 始点から終点へのパスを取得 120
GeneralPath path = new GeneralPath(new Line2D.Float(point1, point2)); 121
// 矢印部分の先端の座標を取得 122
double x = point2.x + barb*Math.cos(theta+Math.PI-phi); 123
double y = point2.y + barb*Math.sin(theta+Math.PI-phi); 124 // 矢印部分の先端へ移動 125 path.moveTo((float)x, (float)y); 126 // 終点へ向かってラインを引く 127 path.lineTo((float)point2.x, (float)point2.y); 128
GraphPanel.java -6- // もうひとつの矢印部分の先端を取得 129 x = point2.x + barb*Math.cos(theta+Math.PI+phi); 130 y = point2.y + barb*Math.sin(theta+Math.PI+phi); 131 // 終点から矢印部分の先端へラインを引く 132 path.lineTo((float)x, (float)y); 133 // 矢印線を表すパスを返す 134 return path; 135 } 136 137 /** 138 * 属性付き文字列から数式を表すオブジェクトを作成<br> 139 * RPNUtil.RPN(数式文字列)で Term オブジェクトを生成できる 140 * @param formula 数式を表す属性付き文字列 141 * @throws RPNException 142 */ 143
public void setFormula(AttributedString formula) throws RPNException { 144 // null だったら何もしない --- 145 if (formula == null) 146 return; 147 // 数式文字列生成 例:x の 2 乗→x^2 など --- 148
StringBuffer sb = new StringBuffer(); // 数式文字列生成用 149
StringBuffer sbSup = new StringBuffer(); // 上付き文字保存用 150
// 属性付き文字列のループのまわし方は以下のようにする 151
// char 変数 ch に文字が順に入り、属性はイテレータ ite が持っている 152
AttributedCharacterIterator ite = formula.getIterator(); 153
for (char ch=ite.first(); ch!=CharacterIterator.DONE; ch=ite.next()) { 154 // 上付き文字属性だったら ch を sbSup に追加 155 if (ite.getAttribute(TextAttribute.SUPERSCRIPT) == TextAttribute.SUPERSCRIPT_SUPER) { 156 if (sbSup.length() == 0) { 157 sb.append('^'); 158 } 159 sbSup.append(ch); 160 } 161 // 上付き文字属性でなければ ch を sb に追加 162 else { 163 // sbSup に何か文字が入ってれば sb に sbSup の内容を追加 164 if (sbSup.length() != 0) { 165 sb.append(sbSup); 166
sbSup.delete(0, sbSup.length()); // sbSup を空にする 167 } 168 sb.append(ch); 169 } 170 } 171 // 最後に sbSup に残っている分を sb に追加 172 if (sbSup.length() != 0) { 173 sb.append(sbSup); 174 sbSup.delete(0, sbSup.length()); 175 } 176 // πは pi に、×は*にする 177
for (int i=0; i<sb.length(); i++) { 178
// if (sb.charAt(i) == 'π') と等価 179
if (sb.charAt(i) == 0x03C0) { 180
sb = sb.replace(i, i+1, "pi"); 181 i++; 182 } 183 // else if (sb.charAt(i) == '×') と等価 184 else if (sb.charAt(i) == 0xD7) { 185 sb = sb.replace(i, i+1, "*"); 186 } 187 } 188 // 数式文字列を解析して数式オブジェクトを作る --- 189 term = RPNUtil.RPN(sb.toString()); 190 } 191 192
GraphPanel.java
-7-
/* 範囲 setter **********************************************************/ 193
194
public void setxMin(double xMin) { 195 this.xMin = xMin; 196 } 197 198
public void setxMax(double xMax) { 199 this.xMax = xMax; 200 } 201 202
public void setyMin(double yMin) { 203 this.yMin = yMin; 204 } 205 206
public void setyMax(double yMax) { 207 this.yMax = yMax; 208 } 209 210 } 211