Closed tsonobe1 closed 1 year ago
Splittable task box
重複している状態を持っていることを判定する 参考:https://cpoint-lab.co.jp/article/202103/19357/
イメージ : https://www.hrupin.com/2017/04/calendar-dayview-for-android 実装 : https://stackoverflow.com/questions/53215825/return-optimized-x-coordinates-to-normalize-maximize-area-for-an-array-of-rectan
Eventのリストをループさせて、
y: Height (StartDate)
type: START
index: Index
y: Height (EndDate)
type: END
index: index
というデータを作成。= EventをStartDateとEndDateの2つに分ける STARTとENDを別データにすることで、後々重複箇所を判定するのに役立つ
これらを、eventQueueに突っ込む。 eventQueueはpriority Queue。
Queueの優先順位は、
y
-> Type
-> ID
の順。
TaskのStartDateを先にカウントしていることで、いくつのTaskがOverlapしているのかを数えることができる。
STARTが連続するたび、overlap
変数に加算++
していき、それをmaxOverlap
にコピーする。
ENDが来たら、overlapを--
していき、0になったらoverlapしている要素が終わったということになる。
overlap--
の処理の際は、maxOverlap
は上書きしない
Startが加算を開始してから、overlap
が0
に戻るまでの要素を、1つのRegion
として、regionQueue
にenqueueしていく。
RegionにはmaxOverlapプロパティがある。
eventのwidthは、dailyCalenderのwidth / maxOverlap
を指定することで、eventを重複せずに表示することができる
import Foundation
import SwiftUI
// MARK: Model
// CoreDataのEntityの再現
struct Task1 {
var name: String
var startDate: Date
var endDate: Date
var id: UUID
}
// Task1をStartDateとEndDateに分割するための構造体
struct TimeMoment {
var time: Date
var type: TimeType
var index: Int
var id: UUID
enum TimeType: Comparable {
case START
case END
// 比較演算子を使用可能にする
static func < (lhs: TimeType, rhs: TimeType) -> Bool {
switch (lhs, rhs) {
case (.START, .END):
return true
default:
return false
}
}
}
}
// MARK: データセットを作る
func makeDataSet() -> [Task1]{
var tasks = [Task1]()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = "2022-12-06"
let date = dateFormatter.date(from: dateString)!
// Data Set
let startDate1 = date
let endDate1 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
let startDate2 = Calendar.current.date(byAdding: .minute, value: 30, to: startDate1)!
let endDate2 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
let startDate3 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
let endDate3 = Calendar.current.date(byAdding: .minute, value: 400, to: startDate1)!
let startDate4 = Calendar.current.date(byAdding: .minute, value: 15, to: startDate1)!
let endDate4 = Calendar.current.date(byAdding: .minute, value: 60, to: startDate1)!
let startDate5 = Calendar.current.date(byAdding: .minute, value: 100, to: startDate1)!
let endDate5 = Calendar.current.date(byAdding: .minute, value: 240, to: startDate1)!
let startDate6 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
let endDate6 = Calendar.current.date(byAdding: .minute, value: 360, to: startDate1)!
let startDate7 = Calendar.current.date(byAdding: .minute, value: 115, to: startDate1)!
let endDate7 = Calendar.current.date(byAdding: .minute, value: 145, to: startDate1)!
tasks.append(Task1(name: "Task 1", startDate: startDate1, endDate: endDate1, id: UUID()))
tasks.append(Task1(name: "Task 2", startDate: startDate2, endDate: endDate2, id: UUID()))
tasks.append(Task1(name: "Task 3", startDate: startDate3, endDate: endDate3, id: UUID()))
tasks.append(Task1(name: "Task 4", startDate: startDate4, endDate: endDate4, id: UUID()))
tasks.append(Task1(name: "Task 5", startDate: startDate5, endDate: endDate5, id: UUID()))
tasks.append(Task1(name: "Task 6", startDate: startDate6, endDate: endDate6, id: UUID()))
tasks.append(Task1(name: "Task 7", startDate: startDate7, endDate: endDate7, id: UUID()))
return tasks
}
var tasks: [Task1] = makeDataSet()
// MARK: TaskをSTART, ENDの2つに分割する
func splitTasksIntoStartAndEndDates(tasks: [Task1]) -> [TimeMoment]{
var timeMoments = [TimeMoment]()
for (index, task) in tasks.enumerated() {
timeMoments.append(TimeMoment(time: task.startDate, type: .START, index: index, id: task.id))
timeMoments.append(TimeMoment(time: task.endDate, type: .END, index: index, id: task.id))
}
return timeMoments
}
var splitedTasks = splitTasksIntoStartAndEndDates(tasks: tasks)
// MARK: 3つのキーでソートする
let comparator = { (a: TimeMoment, b: TimeMoment) -> Bool in
// もし1つめと2つめのtimeが異なるなら、値が小さい方を返す
if a.time != b.time {
return a.time < b.time
}
// もし1つめと2つめのtypeが異なるなら、STARTの方を返す
if a.type != b.type {
return a.type < b.type
} else {
return a.index < b.index
}
}
splitedTasks.sort(by: comparator)
// OverlapしているRegionごとにArrayを作成する
var currentOverlap = Int.zero
var maxOverlap = Int.zero
var regions = [TimeMoment]()
// イベント毎にオーバーラップ数を格納する辞書を作成する
var taskOverlapCounts: [UUID: Int] = [:]
for event in splitedTasks {
regions.append(event)
// イベントの種類に応じてオーバーラップ数を増減
if event.type == .START {
currentOverlap += 1
} else {
currentOverlap -= 1
}
// 最大オーバーラップ数を更新
maxOverlap = max(currentOverlap, maxOverlap)
// オーバーラップ数が0になった場合は、dictに[task.id : maxOverlap]を追加していく
if currentOverlap == 0 {
for i in regions {
taskOverlapCounts[i.id] = maxOverlap
}
regions.removeAll()
maxOverlap = 0
}
}
taskOverlapCounts
for i in tasks {
print("\(i.name)は\(taskOverlapCounts[i.id]!)個のTaskと重なっています")
}
tasks
// MARK: CoredataからfetchしたTasksを回すときに...
// これはcoredataでtask.startDataでソートするのを再現するためのcomparator
let comparator2 = { (a: Task1, b: Task1) -> Bool in
return a.startDate < b.startDate
}
// 各RegionのMaxOverlapを入れておく
var maxOverlapCountInCurrentRegion = 0
// Region内の何個目の要素にアクセス中かをカウントする。x軸に用いる。
var sequenceNumberInCurrentRegion = 0
for i in tasks.sorted(by: comparator2) {
maxOverlapCountInCurrentRegion = taskOverlapCounts[i.id]!
sequenceNumberInCurrentRegion += 1
// 👉 ここでUIの処理
let sampleString = """
--------------------------------------------------
\(i.name)は
\(maxOverlapCountInCurrentRegion)個のTaakと重なっています。
また、\(i.name)は、重なっているTaskのうち、
\(sequenceNumberInCurrentRegion)個目のTaskです
--------------------------------------------------
"""
print(sampleString)
// ⚠️ MARK: 以下の処理は、for loopの末尾に指定すること
// 繰り返し処理をmaxOverlapCountInRegion回行ったら、両者を0にする
if sequenceNumberInCurrentRegion == maxOverlapCountInCurrentRegion {
maxOverlapCountInCurrentRegion; sequenceNumberInCurrentRegion = 0
}
}
//
// TaskOverlapCountInRegion.swift
// imonit
//
// Created by 薗部拓人 on 2022/12/11.
//
import Foundation
import SwiftUI
// Task EntityをStartDateとEndDateに分割するための構造体
struct TimeMoment {
var time: Date
var type: TimeType
var index: Int
var id: UUID
var overlap: Int = 0
// var maxOverlap: Int = 0
enum TimeType: Comparable {
case START
case END
// 比較演算子を使用可能にする
static func < (lhs: TimeType, rhs: TimeType) -> Bool {
switch (lhs, rhs) {
case (.START, .END):
return true
default:
return false
}
}
}
}
// MARK: TaskをSTART, ENDの2つに分割する
func splitTasksIntoStartAndEndDates(tasks: FetchedResults<Task>) -> [TimeMoment]{
var timeMoments = [TimeMoment]()
for (index, task) in tasks.enumerated() {
timeMoments.append(TimeMoment(time: task.startDate!, type: .START, index: index, id: task.id!))
timeMoments.append(TimeMoment(time: task.endDate!, type: .END, index: index, id: task.id!))
}
return timeMoments
}
// MARK: 3つのキーでソートする
let comparator = { (a: TimeMoment, b: TimeMoment) -> Bool in
// もし1つめと2つめのtimeが異なるなら、値が小さい方を返す
if a.time != b.time {
return a.time < b.time
}
// もし1つめと2つめのtypeが異なるなら、STARTの方を返す
if a.type != b.type {
return a.type > b.type
} else {
return a.index < b.index
}
}
// MARK: taskが他のTaskと重なっている数を、task.idに紐づけて返す
func getOverlapCount(splitedTasks: [TimeMoment]) -> [UUID: (maxOverlap: Int, xAxisOrder: Int)]{
// イベント毎にオーバーラップ数を格納する辞書を作成する
var taskOverlapCounts: [UUID: (maxOverlap: Int, xAxisOrder: Int)] = [:]
var currentOverlap = 0
var isHasBeenMinusCurrentOverlap = false
var maxOverlap = 0
var regions = [TimeMoment]()
for event in splitedTasks {
// イベントの種類に応じてオーバーラップ数を増減
if event.type == .START {
currentOverlap += 1
} else {
currentOverlap -= 1
// isHasBeenMinusCurrentOverlap = true
}
// 最大オーバーラップ数を更新
maxOverlap = max(currentOverlap, maxOverlap)
if event.type == .START {
regions.append(
TimeMoment(
time: event.time,
type: event.type,
index: event.index,
id: event.id,
overlap: currentOverlap
)
)
}
// オーバーラップ数が0になった場合は、dictに[task.id : (maxOverlap, xOrder)]を追加していく
if currentOverlap == 0 {
let maxOverlapWhenStartType = maxOverlap
for region in regions{
// taskのidをkeyとして、そのTaskのmaxOverlapと、x軸上の左から何番目に配置すべきかを引き出せるdict
taskOverlapCounts[region.id] = (
// 自身のTaskが所属するRegionのmaxOverlap
maxOverlap: maxOverlapWhenStartType,
// x軸上で、左から何番目に配置されるべきかを示したInt
xAxisOrder: region.overlap
)
regions.removeAll()
maxOverlap = 0
}
}
}
return taskOverlapCounts
}
// MARK: 上記の処理をまとめる
func getOverlapCountAndXAxisWithTaskID(tasks: FetchedResults<Task>) -> [UUID: (maxOverlap: Int, xAxisOrder: Int)] {
var splitedTasks = splitTasksIntoStartAndEndDates(tasks: tasks)
splitedTasks.sort(by: comparator)
let overlapCountWithTaskID = getOverlapCount(splitedTasks: splitedTasks)
return overlapCountWithTaskID
}
getOverlapCountAndXAxisWithTaskIDで得た辞書を、forで回したtask(CoreDataからfetchしたtaskEntity)で使うことで、そのtaskのmaxOverlap(widthの幅を、画面の横幅のなん分割にすべきか)と、xAxisOrder(x軸の左から何番目に配置すればよいか)を取得できる
このようなコンテンツがある場合に、緑枠のTaskBoxが、2の位置に配置されてしまう。 そのため、別の方法を調べていたところ、以下の記事を見つけた。
https://stackoverflow.com/a/11323909/13646439
この回答の上から4つ目
のjsbinのリンク先で、投稿では解決できていなかった問題を解決できていた。
以下スクショ
このjavascriptをswiftで実装した。
次の投稿へ続く
$( document ).ready( function( ) {
var column_index = 0;
$( '#timesheet-events .daysheet-container' ).each( function() {
var block_width = $(this).width();
var columns = [];
var lastEventEnding = null;
// Create an array of all events
var events = $('.bubble_selector', this).map(function(index, o) {
o = $(o);
var top = o.offset().top;
return {
'obj': o,
'top': top,
'bottom': top + o.height()
};
}).get();
// Sort it by starting time, and then by ending time.
events = events.sort(function(e1,e2) {
if (e1.top < e2.top) return -1;
if (e1.top > e2.top) return 1;
if (e1.bottom < e2.bottom) return -1;
if (e1.bottom > e2.bottom) return 1;
return 0;
});
// Iterate over the sorted array
$(events).each(function(index, e) {
// console.log(e.top)
if (lastEventEnding !== null && e.top >= lastEventEnding) {
PackEvents( columns, block_width );
columns = [];
lastEventEnding = null;
}
var placed = false;
for (var i = 0; i < columns.length; i++) {
var col = columns[ i ];
if (!collidesWith( col[col.length-1], e ) ) {
col.push(e);
placed = true;
break;
}
}
if (!placed) {
columns.push([e]);
}
if (lastEventEnding === null || e.bottom > lastEventEnding) {
lastEventEnding = e.bottom;
}
});
if (columns.length > 0) {
PackEvents( columns, block_width );
}
});
});
function PackEvents( columns, block_width )
{
var n = columns.length;
// console.log("---------------")
for (var i = 0; i < n; i++) {
var col = columns[ i ];
for (var j = 0; j < col.length; j++)
{
var bubble = col[j];
console.log((i / n)*100)
bubble.obj.css( 'left', (i / n)*100 + '%' );
bubble.obj.css( 'width', block_width/n - 1 );
}
}
}
function collidesWith( a, b )
{
return a.bottom > b.top && a.top < b.bottom;
}
<body>
<div id="timesheet-events">
<!- ----------------------------------------------------------- -->
<div class="daysheet-container" style="left: 0%;">
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 28.662420382166px; left: 0px; top: 0px; width: 28px; ">
<div class="bubble-body">
<div style="border-color: #0000aa !important;" class="bubble-frame"></div>
<div style="background-color: #0000aa !important; border-color: #0000aa !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 40.108280254777px; top: 318.28571428571px; width: 28px; left: 28px; ">
<div class="bubble-body">
<div style="border-color: #0000aa !important;" class="bubble-frame"></div>
<div style="background-color: #0000aa !important; border-color: #0000aa !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 9.108280254777px; top: 400.7619047619px; width: 28px; left: 56px; ">
<div class="bubble-body">
<div style="border-color: #0000aa !important;" class="bubble-frame"></div>
<div style="background-color: #0000as !important; border-color: #0000aa !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 100.26751592357px; top: 20px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 50.26751592357px; top: 0px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 100.26751592357px; top: 300px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 100.26751592357px; top: 450px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 30.26751592357px; top: 415px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 100.26751592357px; top: 450px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
<!- ----------------------------------------------------------- -->
<div class="bubble-activator">
<div class="bubble_selector" style="height: 100.26751592357px; top: 29px; width: 28px; left: 84px; ">
<div class="bubble-body">
<div style="border-color: #992200 !important;" class="bubble-frame"></div>
<div style="background-color: #992200 !important; border-color: #992200 !important;" class="bubble-back"></div>
</div>
</div>
</div>
以下変換したswift
import Foundation
var taskXPositionAndWidth: [UUID: (xPositionAsFractionOfLength: Double, widthRatio: Double)] = [:]
// MARK: Model
// CoreDataのEntityの再現
struct Task1 {
var name: String
var startDate: Date
var endDate: Date
var id: UUID
}
// MARK: データセットを作る
func makeDataSet() -> [Task1]{
var tasks = [Task1]()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = "2022-12-06"
let date = dateFormatter.date(from: dateString)!
// Data Set
let startDate1 = date
let endDate1 = Calendar.current.date(byAdding: .minute, value: 50, to: startDate1)!
let startDate2 = Calendar.current.date(byAdding: .minute, value: 0, to: startDate1)!
let endDate2 = Calendar.current.date(byAdding: .minute, value: 100, to: startDate1)!
let startDate3 = Calendar.current.date(byAdding: .minute, value: 30, to: startDate1)!
let endDate3 = Calendar.current.date(byAdding: .minute, value: 150, to: startDate1)!
let startDate4 = Calendar.current.date(byAdding: .minute, value: 300, to: startDate1)!
let endDate4 = Calendar.current.date(byAdding: .minute, value: 550, to: startDate1)!
let startDate5 = Calendar.current.date(byAdding: .minute, value: 350, to: startDate1)!
let endDate5 = Calendar.current.date(byAdding: .minute, value: 400, to: startDate1)!
let startDate6 = Calendar.current.date(byAdding: .minute, value: 600, to: startDate1)!
let endDate6 = Calendar.current.date(byAdding: .minute, value: 700, to: startDate1)!
let startDate7 = Calendar.current.date(byAdding: .minute, value: 800, to: startDate1)!
let endDate7 = Calendar.current.date(byAdding: .minute, value: 900, to: startDate1)!
let startDate8 = Calendar.current.date(byAdding: .minute, value: 1000, to: startDate1)!
let endDate8 = Calendar.current.date(byAdding: .minute, value: 1500, to: startDate1)!
let startDate9 = Calendar.current.date(byAdding: .minute, value: 1000, to: startDate1)!
let endDate9 = Calendar.current.date(byAdding: .minute, value: 1500, to: startDate1)!
let startDate10 = Calendar.current.date(byAdding: .minute, value: 50, to: startDate1)!
let endDate10 = Calendar.current.date(byAdding: .minute, value: 150, to: startDate1)!
tasks.append(Task1(name: "Task 1", startDate: startDate1, endDate: endDate1, id: UUID()))
tasks.append(Task1(name: "Task 2", startDate: startDate2, endDate: endDate2, id: UUID()))
tasks.append(Task1(name: "Task 3", startDate: startDate3, endDate: endDate3, id: UUID()))
tasks.append(Task1(name: "Task 4", startDate: startDate4, endDate: endDate4, id: UUID()))
tasks.append(Task1(name: "Task 5", startDate: startDate5, endDate: endDate5, id: UUID()))
tasks.append(Task1(name: "Task 6", startDate: startDate6, endDate: endDate6, id: UUID()))
tasks.append(Task1(name: "Task 7", startDate: startDate7, endDate: endDate7, id: UUID()))
tasks.append(Task1(name: "Task 8", startDate: startDate8, endDate: endDate8, id: UUID()))
tasks.append(Task1(name: "Task 9", startDate: startDate9, endDate: endDate9, id: UUID()))
tasks.append(Task1(name: "Task 10", startDate: startDate10, endDate: endDate10, id: UUID()))
return tasks
}
var tasks: [Task1] = makeDataSet()
// MARK: 開始時刻、終了時刻の順に並べ替える。
let comparator = { (e1: Task1, e2: Task1) -> Bool in
if e1.startDate < e2.startDate { return true }
if e1.startDate > e2.startDate { return false }
if e1.endDate < e2.endDate { return true }
if e1.endDate > e2.endDate { return false }
return false
}
tasks.sort(by: comparator)
var columns: [[Task1]] = []
var lastEventEnding: Date?
var taskOverlapCounts: [UUID: (maxOverlap: Int, xAxisOrder: Int)] = [:]
tasks.enumerated().forEach { (index, task) in
// A 今回のtaskが、前のtaskと時間が被っていない場合
if lastEventEnding != nil && task.startDate >= lastEventEnding! {
packEvents(columns: columns) // ⚠️
columns = [] // columnsのリセット
lastEventEnding = nil
}
// B カラムを順に見て、前のtaskと今のtaskが重なる場合、カラムに今のtaskを追加
var placed = false
for i in 0..<columns.count {
let col = columns[i]
// 前のtaskと今のtaskが重なっていない場合
if !collidesWith( a: col[col.count - 1], b: task ){
// ↓ colに代入をしていて手間取っていたが、swiftは参照渡しじゃないので、columns[i]にしないといけない。
// ↓ javascriptの実装例では、colに代入していたが、jsはオブジェクトが参照渡しなので可能だっただけ。
columns[i].append(task)
placed = true
break
}
}
// C taskがどのカラムにも追加されなかった場合、新しいカラムを作成し、そこに今のtaskを追加
if !placed {
columns.append([task])
}
// D 今のtaskのendDateが前回のtaskのendDateより大きい場合、lastEventEndingを今のtaskのendDateに更新
if lastEventEnding == nil || task.endDate > lastEventEnding! {
lastEventEnding = task.endDate
}
}
if !columns.isEmpty {
packEvents(columns: columns)
}
// 2つのTaskの時間が重なっているかどうかを判定する, 被ってたらtrue
func collidesWith(a: Task1, b: Task1) -> Bool {
a.endDate > b.startDate && a.startDate < b.endDate
}
func packEvents(columns: [[Task1]]) {
let columnsCount = columns.count
print("---------------------------")
for (index, column) in columns.enumerated() {
for task in column {
let leftRate = Double(index) / Double(columnsCount)
let widthRate = 100.0 / Double(columnsCount) / 100.0
print("\(task.name), left \(leftRate), width \(widthRate)")
taskXPositionAndWidth[task.id] = (xPositionAsFractionOfLength: leftRate, widthRatio: widthRate)
}
}
}
print("------------------------Dict----------------------------")
for i in taskXPositionAndWidth {
print(i)
}
最終的に、taskXPositionAndWidthという、x軸上の左からの位置の比率と、x軸上の横幅の比率を、taskのidと紐付けた辞書を算出している。これを、SwiftUIのViewでtaskを描画する際に利用する。
↑ 2つのboxが重なっている.
これらのwitdhを左右に分割して、重ならないようにする