目次
1. この記事の目的
この記事では、xy-平面上の点の情報から角度を求める方法について紹介します。
特に、三点を入力とし、この三点がなす角の外角を求める方法を紹介します。
また、入力された三点をこの順になぞったときに時計回りに曲がっているかを判定できるものにしたいと思います。
具体的には、時計回りに90度曲がったときに-π/2、反時計回りに90度曲がったときにπ/2を返すようにします。
こうすることで、求めた角度の正負で時計回りかどうかを判定できます。
サンプルコードを上げました。
github.com
2. 三点がなす角度の求め方
まずは数学的にどのようなアプローチで三点がなす角度を求めるかを紹介します。
数学のことはいいからコードを見せろ!という方は次の段落「3. x,y-平面上の座標を表すクラス ポイントクラス」まで読み飛ばしてください。
一般的にはベクトルの内積を使用するようです。
ベクトルa, bの内積をa.b、大きさを|a|, |b|、なす角をtとすると、内積は次の通り。
a.b = |a| * |b| * cos(t)
この方程式をtについて解くと、
t = acos( a.b / (|a| * |b|))
これで三点がなす角度がわかったので、180度からこれを引けば外角が求まります。
しかしこのままでは時計回りかどうかは判定できません。
入力の最初と最後を入れ替えても同じ角度が帰ってきてしまいます。
そこで役に立つのが外積です。
外積は、時計回りのときにマイナス、反時計回りのときにプラスになります。
これでプログラミングに移れそうです。
3. x,y-平面上の座標を表すクラス ポイントクラス
では早速プログラミングに入っていきます。
まずは二次元平面上の座標を表すクラスです。
今回はPoint2と名付けることにします。
double型のメンバx, yを持つだけです。
package model; public class Point2 { private double x; private double y; public Point2(double x, double y) { this.x = x; this.y = y; } }
Point2はシンプルなクラスなので、ベクトルとしても扱うことができます。
例えば点p1, p2があったときに、
v(p1 -> p2) = p2 - p1
と計算することができます。
では次の段落でPoint2にベクトル演算用のメソッドを実装していきます。
4. ベクトルの差、長さ、内積、外積
ベクトルの差を返す関数を実装します。
使いやすさ重視で、引き算した結果を新しいインスタンスで返します。
public Point2 subtract(Point2 point) { return new Point2(this.x - point.x, this.y - point.y); }
次にベクトルの長さを返すメソッドを実装します。
public double magnitude() { return Math.sqrt(x * x + y * y); }
次にベクトルの内積を返すメソッドを実装します。 二次元平面のベクトルa, bの内積はax * bx + ay * byで求められます。
public double innerProduct(Point2 point) { return this.x * point.x + this.y * point.y; }
最後にベクトルの外積を返すメソッドを実装します。 二次元平面のベクトルa, bの外積はa.x * b.y - a.y * b.xで求められます。
public double outerProduct(Point2 point) { return this.x * point.y - this.y * point.x; }
5. 三点がなす外角を求める
ここまでで二次元平面上の座標やベクトルの定義と、それらの計算につかうメソッドをいくつか実装しました。 それでは早速、三点がなす外角を求めましょう。
手順としては以下の通りです。
t = acos( a.b / (|a| * |b|)) で内角を求める。
外積の正負で処理を分け、内角から外角を求める。
package util; import model.Point2; public class MathUtil { public static double calculateExternalAngle(Point2 p1, Point2 p2, Point2 p3) { Point2 v1 = p2.subtract(p1); Point2 v2 = p2.subtract(p3); double angleRadian = Math.acos(v1.innerProduct(v2) / (v1.magnitude() * v2.magnitude())); double angleDegree = angleRadian * 180 / Math.PI; if (v1.outerProduct(v2) > 0) { return angleDegree - 180; } else { return 180 - angleDegree; } } }
動作を確認するためテストコードを作成しました。
(動作を見るだけで、アサートなどはおこなっていません)
package util; import model.Point2; import org.junit.Test; public class MathUtilTest { @Test public void calculateExternalAngle() throws Exception { Point2 one = new Point2(1.0, 0.0); Point2 zero = new Point2(0.0, 0.0); for (int angleDegree = 0; angleDegree <= 360; angleDegree += 10) { double angleRadian = angleDegree / 180f * Math.PI; Point2 point = new Point2(Math.cos(angleRadian), Math.sin(angleRadian)); double externalAngleDegree = MathUtil.calculateExternalAngle(one, zero, point); System.out.println(String.format("%4d -> %8.3f", angleDegree, externalAngleDegree)); } } }
calculateExternalAngleの第一引数をPoint2(1, 0)、第二引数をPoint2(0, 0)に固定して、第三引数を単位円上の移動する点にして入力と出力が正しいかたしかめています。
実行結果は下記のとおりです。
意図どおりの返り値が帰ってきました。
0 -> 180.000 10 -> -170.000 20 -> -160.000 30 -> -150.000 40 -> -140.000 50 -> -130.000 60 -> -120.000 70 -> -110.000 80 -> -100.000 90 -> -90.000 100 -> -80.000 110 -> -70.000 120 -> -60.000 130 -> -50.000 140 -> -40.000 150 -> -30.000 160 -> -20.000 170 -> -10.000 180 -> 0.000 190 -> 10.000 200 -> 20.000 210 -> 30.000 220 -> 40.000 230 -> 50.000 240 -> 60.000 250 -> 70.000 260 -> 80.000 270 -> 90.000 280 -> 100.000 290 -> 110.000 300 -> 120.000 310 -> 130.000 320 -> 140.000 330 -> 150.000 340 -> 160.000 350 -> 170.000 360 -> 180.000
6. まとめ
三点がなす外角を求める方法を紹介しました。
ポリゴンの三角形分割で役に立つので、その紹介も行いたいと思います。