こちらの続きです。
流用して、4分木る 2 - C#ATIA
やっと、2度と見たくない程の汚いコードになってしまいましたが
組み合わせの効率化を促す、線形4分木が出来ました。
"KCL_Quadtree.bas" と言う名称の標準モジュールとします。
'KCL' と、先日のDotNET Framework ArrayList ラッパークラス 'cList'
が必要です。
ArrayListラッパークラスをお借りしてみる2 - C#ATIA
'vba KCL_Quadtree Ver0.0.1 'KCL_Quadtree.bas - 標準モジュール 'using-'KCL0.09' -'cList0.0.1' 'モートン順序を利用した4分木空間分割 Option Explicit Private Const MAXLEVEL = 5& '有効空間分割最大レベル Private Const MAXCOUNT = 200& '同一空間内最大数(目安) Private Const DUB_MAX = 1.79769313486231E+308 'Double Max Private Const DUB_MIN = -1.79769313486231E+308 'Double Min Private Const LNG_MAX = 2147483647 'Long Max Private Const DBFG = False 'デバッグ True/False Private m_Level& '分割レベル Private m_Tolerance# 'トレランス Private m_AxisCount& '空間分割時の各軸の最大数 Private m_MinPos '空間最小座標 Private m_Unit '空間単位サイズ Private m_CellCount&() 'レベル毎の空間数 Private m_CellAllCount&() '指定レベルまでの合計空間数 Private m_Tolerance_Offset 'トレランスオフセット 左上右下用 '線形4分木リスト取得 ''' @param :Rngboxs-Collection(array(array(Double),array(Double))-座標値郡 ''' @param :Tolerance-Double-一致トレランス ''' @return:Collection((Collection(long)-その空間Idx ''' (Collection(long))-上位空間Idx Function GetLinerQuadtreeList(ByVal Rngboxs As Collection, ByVal Tolerance#) As Collection Set GetLinerQuadtreeList = Nothing 'cList化 Dim Vols As cList: Set Vols = InitList() Set Vols = Rngbox2Volume(Rngboxs) '座標値郡のIdxList作成 Dim IdxList As cList: Set IdxList = InitRangeList(Vols.Count) '空間内最大数以下の場合、そのまま返す Dim DecidedList As cList: Set DecidedList = InitList() If Vols.Count < MAXCOUNT Then Call DecidedList.Add(InitSpace(IdxList.ToArray())) GoTo FuncEnd End If '初期設定 If Not SetStart(Tolerance, MAXCOUNT) Then MsgBox "設定値が不正です" Exit Function End If '空間情報 If Not SetSpaceInfo(Vols, IdxList) Then '空間が小さすぎる Call DecidedList.Add(InitSpace(IdxList.ToArray())) GoTo FuncEnd End If '空間ポインタ配列 Dim SpaceAryCount&: SpaceAryCount = m_CellAllCount(m_Level) - 1 Dim SpeceAry: SpeceAry = InitRangeAry(SpaceAryCount, -1&) '空間に配置 Dim Space, i&, Idx, Morton& Dim SpaceEnum As cList, UprIdxList As cList For Each Idx In IdxList Morton = GetLinerNumber(Vols.Item(Idx)) If SpeceAry(Morton) < 0 Then Call DecidedList.Add(InitSpace()) SpeceAry(Morton) = DecidedList.Count - 1 End If Call DecidedList.Item(SpeceAry(Morton)).Item(0).Add(Idx) Next '有効空間Idx取得 Set SpaceEnum = GetValidSpaceEnum(SpeceAry) Call SpaceEnum.Sort '上位空間Idx取得-上位空間から行う For Each Idx In SpaceEnum Set UprIdxList = GetUprSpaceIdxList(Idx, SpeceAry) Call UprIdxList.Sort Set DecidedList.Item(SpeceAry(Idx)).Item(1) = _ GetUprAllList(UprIdxList, DecidedList) Next If DBFG Then Call DumpSpaceAllIdx(DecidedList) '上位空間順に再配置 Dim SortList As cList: Set SortList = InitList() For Each Idx In SpaceEnum SortList.Add DecidedList.Item(SpeceAry(Idx)) Next Call SortList.TrinToSize Set DecidedList = SortList If DBFG Then Call DumpSpaceAllIdx(DecidedList) FuncEnd: Set GetLinerQuadtreeList = SpaceList2IdxCol(DecidedList) End Function '*** Quadtree *** '有効な上位空間Idx取得 ''' @param :LinerNum-long-線形4分木Idx ''' @param :SpaAry-Array-空間ポインタ ''' @return:cList(long)-線形4分木Idx Private Function GetUprSpaceIdxList(ByVal LinerNum As Long, _ ByVal SpeAry As Variant) As cList Dim Lst As cList: Set Lst = InitList() Set GetUprSpaceIdxList = Lst '全上位空間Idx Dim N: N = LinerNum Do Until N < 1 N = Fix((N - 1) * 0.25) Lst.Add CLng(N) Loop If Lst.Count < 1 Then Exit Function '有効空間Idx Dim Upr As cList: Set Upr = InitList() Dim Idx For Each Idx In Lst If SpeAry(Idx) >= 0 Then Upr.Add SpeAry(Idx) End If Next Set GetUprSpaceIdxList = Upr End Function '有効空間取得 ''' @param :SpaAry-Array-空間ポインタ ''' @return:cList(long) Private Function GetValidSpaceEnum(ByVal SpaAry As Variant) As cList Dim List As cList: Set List = InitList() Dim i& For i = 0 To UBound(SpaAry) If SpaAry(i) >= 0 Then List.Add i Next Set GetValidSpaceEnum = List End Function '有効空間全VolIdx取得 ''' @param :IdxList-cList ''' @param :SpaList-cList ''' @return:cList(long) Private Function GetUprAllList(ByVal IdxList As cList, ByVal SpaList As cList) As cList Dim UprLst As cList: Set UprLst = InitList() Set GetUprAllList = UprLst If IdxList.Count < 1 Then Exit Function Dim List As cList: Set List = InitList() Dim Idx, Upr For Each Idx In IdxList Set List = FlattenSpace(SpaList.Item(Idx)) If KCL.IsNothing(List) Then GoTo Continue For Each Upr In List If Not UprLst.Contains(Upr) Then UprLst.Add Upr End If Next Continue: Next Set GetUprAllList = UprLst End Function '空間作成 ''' @param :This-Variant ''' @param :Upr-Variant ''' @return:cList(cList,cList) Private Function InitSpace(Optional ByVal This, Optional ByVal Upr) As cList Set InitSpace = InitList() InitSpace.Add InitList(This) InitSpace.Add InitList(Upr) End Function '空間平坦化 ''' @param :Space-cList(cList,cList) ''' @return:cList Private Function FlattenSpace(ByVal Space As cList) As cList Set FlattenSpace = Nothing If Space.Count < 1 Then Exit Function Set FlattenSpace = List_join(Space.ItemObj(0), _ List_diff(Space.ItemObj(0), Space.ItemObj(1))) End Function '線形4分木準備 ''' @param :Tolerance-Double-一致トレランス ''' @param :MaxCount-long-同一空間内最大数(目安) ''' @return:Boolean Private Function SetStart(ByVal Tolerance#, ByVal MAXCOUNT&) As Boolean SetStart = False If Tolerance <= 0 Then Exit Function m_Tolerance = Tolerance ReDim m_CellCount(MAXLEVEL): m_CellCount(0) = 1 ReDim m_CellAllCount(MAXLEVEL): m_CellAllCount(0) = 1 Dim i& For i = 1 To UBound(m_CellCount) m_CellCount(i) = m_CellCount(i - 1) * 4 m_CellAllCount(i) = m_CellAllCount(i - 1) + m_CellCount(i) Next m_Tolerance_Offset = Array(Tolerance * -1#, Tolerance) SetStart = True End Function '空間情報設定 ''' @param :Pnts-cList(cList(array(Double),array(Double))-座標値郡 ''' @param :Idxs-cList(long)-座標値郡Idx ''' @return:Boolean Private Function SetSpaceInfo(ByVal Vols As cList, ByVal Idxs As cList) As Boolean SetSpaceInfo = False Dim SpSize: SpSize = GetSpaceSize_Idx(Vols, Idxs) m_MinPos = AryAdd(SpSize(0), m_Tolerance * -2) Dim W: W = ArySubAry(AryAdd(SpSize(1), m_Tolerance * 2), m_MinPos) If Not SetLevel(W) Then Exit Function m_Unit = AryDiv(W, m_AxisCount) SetSpaceInfo = True End Function '座標値郡から空間サイズ取得 ''' @param :EndPnts-cList(CcList(array(Double),array(Double))-座標値郡 ''' @return:array(array(Double))-0:最小値 1:最大値 Private Function GetSpaceSize_Idx(ByVal EndPnts As cList, ByVal IdxList As cList) As Variant Dim Min: Min = InitRangeAry(1, DUB_MAX) Dim Max: Max = InitRangeAry(1, DUB_MIN) Dim Idx, i&, Pnt 'j& For Each Idx In IdxList For Each Pnt In EndPnts.Item(Idx) 'Obj For i = 0 To 1 If Min(i) > Pnt(i) Then Min(i) = Pnt(i) If Max(i) < Pnt(i) Then Max(i) = Pnt(i) Next Next Next GetSpaceSize_Idx = Array(Min, Max) End Function '空間サイズとトレランスからレベル算出し設定 ''' @param :W-array(Double,Double) ''' @return:Boolean Private Function SetLevel(ByVal W) As Boolean SetLevel = False Dim Min#: Min = DUB_MAX Dim i& For i = 0 To 1 If Min > W(i) Then Min = W(i) Next Dim TmpLv&: TmpLv = Fix(Log_n((Min / m_Tolerance * 2), 2)) If TmpLv > MAXLEVEL Then m_Level = MAXLEVEL Else m_Level = TmpLv End If If m_Level < 1 Then Exit Function m_AxisCount = sl(1, m_Level) SetLevel = True End Function '線形4分木RangeBoxに変換 左上と右下 ''' @param :Vol-cList(array(Double),array(Double))-ボリューム座標値 ''' @return:cList(array(Double),array(Double)) Private Function GetMortonRngBox(ByVal Vol As cList) As cList If Vol(0)(0) > Vol(1)(0) Then Call SwapNum(Vol(0)(0), Vol(1)(0)) If Vol(0)(1) < Vol(1)(1) Then Call SwapNum(Vol(0)(1), Vol(1)(1)) Dim Mrb As cList: Set Mrb = InitList() Mrb.Add Vol(0) Mrb.Add Vol(1) Set GetMortonRngBox = Mrb End Function 'ボリュームから線形4分木インデックスNoを取得 ''' @param :Vol-cList(array(Double),array(Double))-ボリューム座標値 ''' @return:Long Private Function GetLinerNumber(ByVal Vol As cList) As Long '点のモートンNoを算出 Dim SwVol: Set SwVol = GetMortonRngBox(Vol) Dim Lt&: Lt = GetPointElem(AryAddAry(SwVol(0), m_Tolerance_Offset)) Dim Rb&: Rb = GetPointElem(AryAddAry(SwVol(1), AryMul(m_Tolerance_Offset, -1))) 'Xorで所属レベル取得 Dim Def&: Def = Rb Xor Lt Dim HiLevel& If Def = 0 Then HiLevel = 0 '最小レベルで同一空間番号 Else HiLevel = 1 Dim i&, Check& For i = 0 To m_Level Check = (sr(Def, (i * 2))) And &H3 If Not Check = 0 Then HiLevel = i + 1 Next End If 'ボリュームの空間番号とレベルから線形4分木インデックスNoを算出 Dim SpaceNum&: SpaceNum = sr(Rb, (HiLevel * 2)) Dim AddNum&: AddNum = (m_CellCount(m_Level - HiLevel) - 1) / 3 SpaceNum = SpaceNum + AddNum '不正データチェック If SpaceNum > m_CellAllCount(m_Level) - 1 Then GetLinerNumber = LNG_MAX End If GetLinerNumber = SpaceNum End Function 'ビット分割関数2D ''' @param :n-long ''' @return:long Private Function BitSeparateFor2D(ByVal N&) As Long Dim S&: S = N S = (S Or sl(S, 8)) And &HFF00FF S = (S Or sl(S, 4)) And &HF0F0F0F S = (S Or sl(S, 2)) And &H33333333 BitSeparateFor2D = (S Or sl(S, 1)) And &H55555555 End Function '4分木モートン順序算出関数 ''' @param :x-long ''' @param :y-long ''' @return:long Private Function Get2DMortonNumber(ByVal x&, ByVal y&) As Long Get2DMortonNumber = BitSeparateFor2D(x) Or _ sl(BitSeparateFor2D(y), 1) End Function '線形4分木インデックス取得関数 ''' @param :Pos-array(Double) ''' @return:long Private Function GetPointElem(ByVal Pos As Variant) As Long GetPointElem = Get2DMortonNumber(Fix((Pos(0) - m_MinPos(0)) / m_Unit(0)), _ Fix((Pos(1) - m_MinPos(1)) / m_Unit(1))) End Function '*** Math *** '配列と配列の足し算 Private Function AryAddAry(ByVal A, ByVal B) As Variant AryAddAry = Array(A(0) + B(0), A(1) + B(1)) End Function '配列同士の引き算 Private Function ArySubAry(ByVal A, ByVal B) As Variant ArySubAry = Array(A(0) - B(0), A(1) - B(1)) End Function '配列と実数の足し算 Private Function AryAdd(ByVal A, ByVal B#) As Variant AryAdd = Array(A(0) + B, A(1) + B) End Function '配列と実数の掛け算 Private Function AryMul(ByVal A, ByVal B#) As Variant AryMul = Array(A(0) * B, A(1) * B) End Function '配列と実数の割り算 Private Function AryDiv(ByVal A, ByVal B#) As Variant AryDiv = Array(A(0) / B, A(1) / B) End Function 'nを底とする対数 Private Function Log_n(x, N) Log_n = Log(x) / Log(N) End Function ' 左シフト Private Function sl(ByVal x&, ByVal N&) As Long If N = 0 Then sl = x Else Dim k: k = CLng(2 ^ (32 - N - 1)) Dim D: D = x And (k - 1) Dim C: C = D * CLng(2 ^ N) If x And k Then C = C Or &H80000000 sl = C End If End Function ' 右シフト(算術(>>)ではなく論理(>>>)シフトに相当) Private Function sr(ByVal x&, ByVal N&) As Long If N = 0 Then sr = x Else Dim y: y = x And &H7FFFFFFF Dim z If N = 32 - 1 Then z = 0 Else z = y \ CLng(2 ^ N) 'ひょっとしたらCLng要らないかも End If If y <> x Then z = z Or CLng(2 ^ (32 - N - 1)) sr = z End If End Function '*** List/Array/Collection *** 'リストの作成 Private Function InitList(Optional ByVal Data) As cList Dim List As cList: Set List = New cList Set InitList = List If IsMissing(Data) Then Exit Function Select Case True Case IsArray(Data) Dim i& For i = 0 To UBound(Data) List.Add Data(i) Next Case TypeName(Data) = "Collection" Dim V For Each V In Data List.Add V Next Case Else List.Add V End Select End Function '初期化済みリスト生成 Private Function InitRangeList(ByVal Count&) As cList Dim List As cList: Set List = InitList() Dim i& For i = 0 To Count - 1 List.Add i Next Set InitRangeList = List End Function 'Lst1に対しLst2の重複を削除したcListを返す Private Function List_diff(ByVal Lst1 As cList, ByVal Lst2 As cList) As cList Set List_diff = Nothing If Lst2.Count < 1 Then Exit Function If Lst1.Count < 1 Then Set List_diff = Lst2 Exit Function End If Dim Lst As cList: Set Lst = InitList() Dim V For Each V In Lst2 If Not Lst.Contains(V) Then Lst.Add V Next Set List_diff = Lst End Function 'cList連結 Private Function List_join(ByVal Lst1 As cList, ByVal Lst2 As cList) As cList Set List_join = Nothing If KCL.IsNothing(Lst1) And KCL.IsNothing(Lst2) Then Exit Function If KCL.IsNothing(Lst1) Then Set List_join = Lst2: Exit Function If KCL.IsNothing(Lst2) Then Set List_join = Lst1: Exit Function Dim Diff As cList: Set Diff = List_diff(Lst1, Lst2) Dim Lst As cList: Set Lst = List_DeepCopy(Lst1) Dim V For Each V In Diff Lst.Add V Next Set List_join = Lst End Function 'cListクローン Private Function List_DeepCopy(ByVal Lst As cList) As cList Set List_DeepCopy = Nothing If KCL.IsNothing(Lst) Then Exit Function Dim DC As cList: Set DC = InitList() DC.capacity = Lst.Count + 1 Dim V For Each V In Lst DC.Add V Next Set List_DeepCopy = DC End Function '初期化済み配列生成 - オブジェクトNG Private Function InitRangeAry(ByVal Count&, ByVal Value As Variant) Dim Ary() As Variant: ReDim Ary(Count) Dim i& For i = 0 To Count Ary(i) = Value Next InitRangeAry = Ary End Function 'コレクションの作成 'OffsetはcListとコレクションのインデックス違いを吸収する苦肉の策 Private Function InitCol(Optional ByVal Data, Optional ByVal Offset) As Collection Dim Col As Collection: Set Col = New Collection Set InitCol = Col If IsMissing(Data) Then Exit Function Select Case True Case IsArray(Data) Dim i& For i = 0 To UBound(Data) Col.Add Data(i) Next Case TypeName(Data) = "cList" Dim V If IsMissing(Offset) Then For Each V In Data Col.Add V Next Else For Each V In Data Col.Add V + Offset Next End If Case Else Col.Add V End Select End Function '*** etc *** 'Swap 値型 Private Sub SwapNum(ByRef A, ByRef B) Dim tmp: tmp = A: A = B: B = tmp End Sub '座標郡の型変換 ''' @param :Col-Collection(Collection(array(Double),array(Double)) ''' @return:cList(cList(array(Double),array(Double)) Private Function Rngbox2Volume(ByVal Rngboxs As Collection) As cList Dim Lst As cList: Set Lst = InitList() Dim Rb For Each Rb In Rngboxs Lst.Add InitList(Rb) Next Set Rngbox2Volume = Lst End Function 'Idx郡の型変換 ''' @param :Col-Collection(Collection(array(Double),array(Double)) ''' @return:cList(cList(array(Double),array(Double)) Private Function SpaceList2IdxCol(ByVal SpaceList As cList) As Collection Dim Col As Collection: Set Col = New Collection Dim Spa As cList, UprLow As Collection, Lv For Each Spa In SpaceList Set UprLow = InitCol() UprLow.Add InitCol(Spa.Item(0), 1) UprLow.Add InitCol(Spa.Item(1), 1) Col.Add UprLow Next Set SpaceList2IdxCol = Col End Function '*** debug *** '確認用 Private Sub DumpQuadIdx(ByVal PosList) Dim Pos Debug.Print " **** " For Each Pos In PosList Debug.Print "Pos:" & Pos(0) & "," & Pos(1), _ "MotonNo:" & GetPointElem(Pos) Next End Sub Private Sub DumpSpaceAllIdx(ByVal Spaces) ' As cList) Dim S, U, T, i&, Fs Debug.Print " **** " For Each S In Spaces If TypeName(S) = "cList" Then Fs = 0 Else Fs = 1 End If If S.Item(Fs).Count < 1 Then T = "無し" Else If TypeName(S) = "cList" Then T = Join(S.Item(Fs).ToArray(), ",") Else Set T = InitList(S.Item(Fs)) T = Join(T.ToArray(), ",") End If End If If S.Item(Fs + 1).Count < 1 Then U = "無し" Else If TypeName(S) = "cList" Then U = Join(S.Item(Fs + 1).ToArray(), ",") Else Set U = InitList(S.Item(Fs + 1)) U = Join(U.ToArray(), ",") End If End If Debug.Print "This: " & T Debug.Print "Upr : " & U Debug.Print Next End Sub '*********************** 'ver0.0.1 - 完成
あまりに複雑すぎて、空間の再分割は諦めました。
GetLinerQuadtreeList関数に
第一引数 - 曲線のレンジボックス郡
第二引数 - トレランス
で投げると、レンジボックスと同一のインデックスが、空間分割されたコレクションとして
戻ってきます。
戻り値は、 Collection(単位空間(空間内インデックス郡,上位空間インデックス郡))
の状態です。(他人にはわかり難い・・・自分でも忘れそう)
これにより、オリジナルで行っている衝突リストの作成を避け、全空間を処理する
為の複雑なループ?処理を行わずに、単純に戻り値のコレクションを走査するだけで
終了できるようにしました。
重複線を処理する際の組み合わせは、
・空間内インデックス同士の組み合わせで重複チェック
・空間内インデックスと上位空間インデックスの組み合わせで重複チェック
の2段階となりますが、恐らく大幅に組み合わせ数を減らす事が可能では
ないかな? と思っています。
これを利用したマクロは次回に。
出来れば、これを再度修正したりしたくないです・・・。