はい、では WireFragment(以下 WF) の集合をエッジの集合に変換する to_edges
メソッドを作っていきます。
LibreOffice Draw で描いた大元のデータはこう。 要素が多いとデバッグが面倒なので少なめにしました。
目標イメージはこんな感じです。
※ 変換後の出力はグラフなんですが、入力もまたグラフなので、ややこしくならないように 今回は入力データでは「点」「WF」、出力データでは「ノード」「エッジ」と呼び方を分けます。
次の図のように、開始点から出発してエッジのもう一方の終端まで WF を辿っていけばいいんじゃないでしょうか。
で、1本のエッジが取り出せたら、また次のエッジを取り出し、全部取り出せるまで繰り返すと。
このとき、通過した WF にフラグを立てておいて、一度通ったところをまた通らないようにします。
おおざっぱにはこんな感じでどうでしょうか。
では細かいところを詰めましょう。
まずは出発点をリストアップする必要があります。
直感的には図で赤丸を付けている点です。 すべての点の中から、何らかの条件でふるい分けて赤丸の点だけ探し出したい。
これは入力のグラフの次数(グラフ理論の用語。各点につながっている WF の数)を見るだけで分かります。
見ての通りですね。次数が 2 ではない点を選べば、それが出発点(兼、終着点)です。
というわけで、まずは点(のセル座標)から次数を引くためのマッピング(以下、次数マップ)を用意します。これは簡単。
def make_degree_map(wf_set) map = Hash.new(0) wf_set.each { |wf| map[wf.pos1] += 1 map[wf.pos2] += 1 } map end degree_map = make_degree_map(wf_set)
次数マップができたら、それを使って出発点のリストを作るのも簡単。
def select_start_points(degree_map) pts = [] degree_map.each { |pt, degree| pts << pt if degree != 2 } pts end start_pts = select_start_points(degree_map)
処理に都合のよいデータをもう1つ用意します。
{ pt1 => [ wf11, wf12, ... ], pt2 => [ wf21, wf22, ... ] }
要するにつながっている順に WF を辿っていきたいので、そのために都合がよいデータです。
たとえば点 pt1 から出発した場合、 pt1 => wf11 => pt11 のように、 WF と WF を経由した次の点 pt11 が分かり、 同じように点 pt11 から出発して次の WF と点を pt11 => wf… => pt... と辿っていくことができます。
こういうデータ構造はグラフのアルゴリズムではよく使われるらしく、「隣接リスト」と呼ばれているそうです。
def make_pt_wfs_map(wf_set) pt_set = Set.new wf_set.each { |wf| pt_set << wf.pos1 pt_set << wf.pos2 } map = {} # 空配列で初期化 pt_set.each { |pt| map[pt] = [] } wf_set.each { |wf| map[wf.pos1] << wf map[wf.pos2] << wf } map end pt_wfs_map = make_pt_wfs_map(wf_set)
データが用意できたのでエッジを取り出す処理を書きます。
def take_edge(degree_map, pt_wfs_map, pt0, wf1) wfs = [] wf1.visit() wfs << wf1 work_pt = wf1.opposite_pos(pt0) loop do next_wfs = if degree_map[work_pt] == 2 pt_wfs_map[work_pt].select { |wf| ! wf.visited } else # 次数が 2 以外の場合は次の経路なし [] end case next_wfs.size when 0 break when 1 # OK else # assert raise "next_wfs.size must be 0 or 1" end next_wf = next_wfs[0] next_wf.visit() wfs << next_wf work_pt = next_wf.opposite_pos(work_pt) end Unit::Edge.new( pt0, work_pt, wfs ) end
next_pt = wf.opposite_pos(pt)
という操作により、WF を挟んで pt
と反対側の点 next_pt
(次に進むべき点)が得られるようにしています。
Edge
には、両方の端点と、WF の配列を持たせておきます。
WF の配列そのままだと無駄が多いのでスリムにしたいところですが、先に進みたいので放置。
これらをまとめて、
to_edges()
はこう。
def to_edges(wf_set) degree_map = make_degree_map(wf_set) start_pts = select_start_points(degree_map) pt_wfs_map = make_pt_wfs_map(wf_set) edges = [] start_pts.each { |start_pt| pt_wfs_map[start_pt].each { |wf| next if wf.visited edges << take_edge(degree_map, pt_wfs_map, start_pt, wf) } } edges end edges = to_edges(wf_set)
描画するときも wf_set
を直接使うのではなく、
edges
を使うようにしてみます。
edges.each { |edge| edge.wfs.each { |wf| drawer.draw_line( wf.x1 + 0.5, wf.y1 + 0.5, wf.x2 + 0.5, wf.y2 + 0.5, C_WHITE ) } }
できました! (エッジの色とノードの赤い点は適当に描き加えたものです)