手書認識 グラフ描画 Step3 認識した数式をもとに関数グラフを描画する < 数式の構造解析 > 一般に 1+3 などと文字列で数式をうけとっても コンピュータはそれをそのままで式とは認識しない あくまで 文字列は文字の並びであり そこに数学的な意味は含まれない 数式として計算するためには プログラムによって数式の構造を解析し コンピュータが計算できる形式に変換する必要がある 今回のプログラムでは 通常の記法でかかれた数式の文字列を 逆ポーランド記法 (Reverse Polish Notation, RPN) という数式の書き方に変換して計算している これは 数字などを演算子の後に記述する書き方で コンピュータとの親和性が高い 本演習では 数式解析して RPN に変換し 計算をする部分はソースごと提供するので 使い方のみ説明する たとえば x 2 + 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); // 描画 -1-
のようにする 上の例では GeneralPath を new するときに コンストラクタに何も指定していないが 直線を表す Line, 四角形を表す Rectangle などを引数に与えてもよい 矢印を作成するメソッドの引数矢印を表す GeneralPath を生成するために getarrowpath(point1, point2, barb, degree) というメソッドを定義して使っている 引数の意味は 次の図にあるとおりである barb degree point2 point1 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 型である -2-
< 作成手順 > 1.rpn.zip を適当な場所で解凍し rpn フォルダをコピーしてプロジェクトの src フォルダ (D: workspace Graph src) の中に貼り付ける 2. パッケージ エクスプローラで Graph プロジェクトを選択し 右クリック リフレッシュ rpn パッケージがあることを確認 3. のソースを編集 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 文字ずつ入力 認識し グラフ描画ボタンを押す グラフが正しく描画されることを確認する -3-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package graph; import java.awt.color; import java.awt.graphics; import java.awt.graphics2d; import java.awt.point; import java.awt.font.textattribute; import java.awt.geom.generalpath; import java.awt.geom.line2d; import java.awt.geom.point2d; import java.text.attributedcharacteriterator; import java.text.attributedstring; import java.text.characteriterator; import javax.swing.jpanel; import rpn.rpnexception; import rpn.rpnutil; import rpn.term; public class GraphPanel extends JPanel { private Term term; // 数式オブジェクト private double xmin=-10, xmax=10; // x の表示範囲 ( 関数の座標 ) private double ymin=-10, ymax=10; // y の表示範囲 ( 関数の座標 ) private int divnum = 200; // グラフ線の刻み幅 * コンストラクタ ( 何もしない ) public GraphPanel() { * 描画メソッド @Override public void paintcomponent(graphics g) { super.paintcomponent(g); Graphics2D g2d = (Graphics2D)g; // 白色背景 g2d.setcolor(color.white); g2d.fillrect(0, 0, getwidth(), getheight()); // グラフ描画 if (term!= null) { // 座標軸 ------------------------------------------- // 黒色設定 g2d.setcolor(color.black); // 原点を取得 Point origin = getgraphicalcoordinate(0, 0); // x 軸の矢印の始点と終点を取得 /* getarrowpath で使うので Point2D.Float にする * これは座標を float で保持する点のクラス Point2D.Float pf1 = new Point2D.Float(0, origin.y); Point2D.Float pf2 = new Point2D.Float(getWidth(), origin.y); // x 軸の矢印を取得 GeneralPath arrow = getarrowpath(pf1, pf2, 10, 20); // x 軸の矢印を描画 g2d.draw(arrow); // y 軸の矢印の始点と終点を取得 pf1 = new Point2D.Float(origin.x, getheight()); pf2 = new Point2D.Float(origin.x, 0); // y 軸の矢印を取得 arrow = getarrowpath(pf1, pf2, 10, 20); -4-
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 // y 軸の矢印を描画 g2d.draw(arrow); // 関数線 -------------------------------------------- // 赤色設定 g2d.setcolor(color.red); double x1,y1,x2,y2; Point p1, p2; // divnum 個の刻みで関数線を描画 for (int i=0; i<divnum; i++) { // i 番目の点を計算 // 数式オブジェクト term.func(x 座標 ) で y 座標を取得可能 x1 = xmin + (xmax-xmin)*i/(double)divnum; y1 = term.func(x1); // i+1 番目の点を計算 x2 = xmin + (xmax-xmin)*(i+1)/(double)divnum; y2 = term.func(x2); // 2 つの点の描画すべき点を取得 p1 = getgraphicalcoordinate(x1, y1); p2 = getgraphicalcoordinate(x2, y2); // パネルの中だったら描画する if (p1.y >= 0 && p1.y <= getheight() && p2.y >= 0 && p2.y <= getheight()) // 直線を描画 g2d.drawline(p1.x, p1.y, p2.x, p2.y); * 関数の座標系からパネルの座標を取得 * @param x * @param y * @return パネル上の点 private Point getgraphicalcoordinate(double x, double y) { return new Point((int)(getWidth()*(x-xMin)/(xMax-xMin)), (int)(getheight() - getheight()*(y-ymin)/(ymax-ymin))); * パスによって矢印線を表すオブジェクトを取得 <br> * point2 に矢印をつける * @param point1 始点 * @param point2 終点 * @param barb 矢印部分の長さ * @param degree 矢印部分の角度 (degree) * @return 矢印を表すパス private GeneralPath getarrowpath(point2d.float point1, Point2D.Float point2, int barb, int degree) { // 角度をラジアンにする double phi = Math.toRadians(degree); // 始点と終点によって決まる線分の角度を求める double theta = Math.atan2(point2.y - point1.y, point2.x - point1.x); // 始点から終点へのパスを取得 GeneralPath path = new GeneralPath(new Line2D.Float(point1, point2)); // 矢印部分の先端の座標を取得 double x = point2.x + barb*math.cos(theta+math.pi-phi); double y = point2.y + barb*math.sin(theta+math.pi-phi); // 矢印部分の先端へ移動 path.moveto((float)x, (float)y); // 終点へ向かってラインを引く path.lineto((float)point2.x, (float)point2.y); -5-
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 // もうひとつの矢印部分の先端を取得 x = point2.x + barb*math.cos(theta+math.pi+phi); y = point2.y + barb*math.sin(theta+math.pi+phi); // 終点から矢印部分の先端へラインを引く path.lineto((float)x, (float)y); // 矢印線を表すパスを返す return path; * 属性付き文字列から数式を表すオブジェクトを作成 <br> * RPNUtil.RPN( 数式文字列 ) で Term オブジェクトを生成できる * @param formula 数式を表す属性付き文字列 * @throws RPNException public void setformula(attributedstring formula) throws RPNException { // null だったら何もしない ---------------------------------------------- if (formula == null) return; // 数式文字列生成例 :x の 2 乗 x^2 など ---------------------------------- StringBuffer sb = new StringBuffer(); // 数式文字列生成用 StringBuffer sbsup = new StringBuffer(); // 上付き文字保存用 // 属性付き文字列のループのまわし方は以下のようにする // char 変数 ch に文字が順に入り 属性はイテレータ ite が持っている AttributedCharacterIterator ite = formula.getiterator(); for (char ch=ite.first(); ch!=characteriterator.done; ch=ite.next()) { // 上付き文字属性だったら ch を sbsup に追加 if (ite.getattribute(textattribute.superscript) == TextAttribute.SUPERSCRIPT_SUPER) { if (sbsup.length() == 0) { sb.append('^'); sbsup.append(ch); // 上付き文字属性でなければ ch を sb に追加 else { // sbsup に何か文字が入ってれば sb に sbsup の内容を追加 if (sbsup.length()!= 0) { sb.append(sbsup); sbsup.delete(0, sbsup.length()); // sbsup を空にする sb.append(ch); // 最後に sbsup に残っている分を sb に追加 if (sbsup.length()!= 0) { sb.append(sbsup); sbsup.delete(0, sbsup.length()); // πは pi に は * にする for (int i=0; i<sb.length(); i++) { // if (sb.charat(i) == 'π') と等価 if (sb.charat(i) == 0x03C0) { sb = sb.replace(i, i+1, "pi"); i++; // else if (sb.charat(i) == ' ') と等価 else if (sb.charat(i) == 0xD7) { sb = sb.replace(i, i+1, "*"); // 数式文字列を解析して数式オブジェクトを作る ---------------------- term = RPNUtil.RPN(sb.toString()); -6-
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 /* 範囲 setter ********************************************************* public void setxmin(double xmin) { this.xmin = xmin; public void setxmax(double xmax) { this.xmax = xmax; public void setymin(double ymin) { this.ymin = ymin; public void setymax(double ymax) { this.ymax = ymax; -7-