はじめに
前々回、前回と、平面走査法による線分交差検出について説明してきました。今回はその仕上げとして、いよいよ平面走査法のアルゴリズムの実装に挑戦します。
前回の復習
まず、前回導入した、平面走査法に必要な3つのデータ構造を復習しておきましょう。
- イベントは、線分の始点と終点、そして交点に対応するオブジェクトです。前回は、イベントを表現するEventクラスを作成しました。
- イベントキューは、イベントを要素とするコレクションです。キューの要素は、イベント点のy座標が小さい順に、常にソートされています。前回、イベントキューには、java.util.TreeSetクラスを利用することを説明しました。
- ステータスは、現在の走査線と交わる線分を要素とするコレクションです。ステータスの要素は、走査線と線分の交点のx座標が小さい順に、常にソートされています。ステータスにもTreeSetクラスを利用します。
この3つを使って交差検出のアルゴリズムを組み立てることが、今回のテーマになります。
イベントの処理
イベントキューには、未処理のイベント点が上から順番に格納されています。走査線を次のイベント点の位置まで動かす操作は、イベントキューから先頭の(最小のy座標を持つ)イベントを取り出す作業に対応します。
さて、こうしてイベントを取り出した後は、そのイベントに関してどんな処理を実行すれば良いのでしょうか。イベントの種類ごとに見ていきましょう。
始点イベントの処理
始点イベントをきっかけとして、その始点を持つ線分(Sstartとします)は、走査線と交わり始めます。したがって、Sstartをステータスに挿入する必要があります。
さらにこのとき、Sstartと以下の線分の交差をチェックします。
- ステータス中でSstartの直前(左隣)に位置する線分(Sl)
- ステータス中でSstartの直後(右隣)に位置する線分(Sr)
もし交差する場合には、その交点イベントを新しく作成し、イベントキューに登録します。
図1は始点イベントの例です。この図の場合、SstartとSrの交差が検出されます(赤い交点)。
交点イベントの処理
交点イベントには、交差する2つの線分が関わります。この2線分のうち、交点通過前の走査線上で左にあった線分をS'l、右にあった線分をS'rとします。
交点を境にして、S'lとS'rの走査線上の位置関係は逆転し、S'lが右に、S'rが左に来るようになります。したがって、ステータス上でのS'lとS'rの位置を入れ替える必要があります。
このとき、ステータスの線分の隣接関係が変化した部分に関して、以下の交差をチェックします。
- 元のステータス中でS'lの直前(左隣)に位置した線分とS'r
- 元のステータス中でS'rの直後(右隣)に位置した線分とS'l
それぞれ、交差する場合には、始点イベントでの交点検出時と同様に、交点イベントを作成してイベントキューに登録します(図2)。
終点イベントの処理
走査線が終点イベントに着くと、その終点を持つ線分(Sendとします)は、今後走査線と交わらなくなります。したがって、Sendをスタータスから削除する必要があります。
ここで、以下の線分を考えます。
- ステータス中でSendの直前(左隣)に位置する線分(S''l)
- ステータス中でSendの直後(右隣)に位置する線分(S''r)
S''lとS''rはこれまで、ステータス上で離れた位置にありました。しかし、Sendを削除したことによって、今後は隣接するようになります。したがって、このS''lとS''rの交差を新たにチェックし、交差する場合には交点イベントを作成・登録します(図3)。
以上をまとめると、表1のようになります。
表1 イベントの処理
種類 | ステータスの変更 | 交差チェック対象 |
始点 | 線分を挿入 | 挿入した線分とその左隣/右隣 |
交点 | 2線分の位置を交換 | 新しく左に来る線分とその左隣/新しく右に来る線分とその右隣 |
終点 | 線分を削除 | 削除する線分の左隣と右隣 |
平面走査法の実装
それでは、ここまで解説してきたアルゴリズムをコーディングしてみましょう。IntersectionDetectorインターフェースを実装するPlaneSweepIntersectionDetectorクラスを、以下のように作成します。
プログラムではまず、与えられたすべての線分について始点イベントと終点イベントを作成し、イベントキューに登録しています。このとき、線分の2端点のうち、上にある方を始点、下にある方を終点としています。こうしてイベントキューを初期化したら、後はイベントを先頭から1つずつ取り出し、表1にしたがって処理を行っていきます。イベントが全部消化され、イベントキューが空になったとき、すべての交差が戻り値として得られていることになります。
ただし、今回の実装ではプログラムを簡潔にするため、3つ以上の線分が1点で交わるなどの例外的な状況は考慮していません。
平面走査法の計算量
平面走査法による線分交差検出の計算量は、入力となる線分の数をn、交点の数をkとすると
で表されます。交点数に依存した計算量となるため、全ての線分が互いに交わっているような、極端に「こんがらがった」配置に対しては、かえって遅くなってしまいます。しかし、交点検出が必要になる実際の状況では、せいぜい線分数に比例する程度の交点しか存在しないことが一般的なので、前々回で実装した総当たり法よりも計算量を大きく削減できることになります。
デモプログラム
今回解説したアルゴリズムをグラフィカルに動作確認できるよう、Swingアプリケーションのデモプログラムを用意しました。Java Web Start形式になっており、 こちらをクリックして直接起動できます。ソースコードは紙面には掲載しませんので、アーカイブをダウンロードして確認してください。
デモプログラムを起動すると、キャンバスにいくつかの線分が表示されます。「移動」「追加」「削除」のツールボタンを選択し、キャンバス上でマウスクリックやドラッグをすることで、線分を自由に追加したり、動かしたりすることができます。「次のイベント」ボタンをクリックすると、走査線が次のイベント点まで移動し、イベント点に関係する線分や、このとき検出される交点が強調表示されます。線分の配置をいろいろと変更し、処理の流れを追ってみてください。
まとめと次回予告
今回は、平面走査法による線分交差検出の完全な実装を行いました。走査線を少しずつ動かしていくという平面走査法の発想は、交差の検出に限らず、平面幾何のさまざまな問題に広く適用できますので、ぜひ覚えておいてください。
今回作成したプログラムのソースコードがダウンロードできます。
次回からは、線分を連結して作られる凸状の多角形について考えていきます。お楽しみに!