166 lines
5.3 KiB
C#
166 lines
5.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.UI.Xaml;
|
|
using Wino.Calendar.ViewModels.Data;
|
|
using Wino.Core.Domain.Models.Calendar;
|
|
|
|
namespace Wino.Calendar.Controls;
|
|
|
|
public sealed class TimedItemLayout
|
|
{
|
|
public TimedItemLayout(CalendarItemViewModel item, int dayIndex, DateOnly date, LayoutRect bounds, DataTemplate? template = null)
|
|
{
|
|
Item = item;
|
|
DayIndex = dayIndex;
|
|
Date = date;
|
|
Bounds = bounds;
|
|
Template = template;
|
|
}
|
|
|
|
public CalendarItemViewModel Item { get; set; }
|
|
public int DayIndex { get; set; }
|
|
public DateOnly Date { get; set; }
|
|
public LayoutRect Bounds { get; set; }
|
|
public DataTemplate? Template { get; set; }
|
|
}
|
|
|
|
internal sealed record TimedCalendarLayoutResult(IReadOnlyList<DateOnly> VisibleDates, double DayWidth, IReadOnlyList<TimedItemLayout> Items);
|
|
|
|
internal static class TimedCalendarLayoutCalculator
|
|
{
|
|
public static double GetTimelineHeight(double hourHeight) => hourHeight * 24d;
|
|
|
|
public static TimedCalendarLayoutResult Calculate(VisibleDateRange range, IEnumerable<CalendarItemViewModel> items, double availableWidth, double hourHeight)
|
|
{
|
|
var visibleDates = range.Dates;
|
|
var dayWidth = visibleDates.Count == 0 ? 0d : availableWidth / visibleDates.Count;
|
|
var layouts = new List<TimedItemLayout>();
|
|
|
|
for (var dayIndex = 0; dayIndex < visibleDates.Count; dayIndex++)
|
|
{
|
|
var date = visibleDates[dayIndex];
|
|
var daySegments = BuildDaySegments(items, date)
|
|
.OrderBy(segment => segment.StartMinute)
|
|
.ThenBy(segment => segment.EndMinute)
|
|
.ToList();
|
|
|
|
foreach (var cluster in BuildClusters(daySegments))
|
|
{
|
|
AssignColumns(cluster);
|
|
var columnCount = cluster.Max(segment => segment.ColumnIndex) + 1;
|
|
var subColumnWidth = columnCount == 0 ? dayWidth : dayWidth / columnCount;
|
|
|
|
foreach (var segment in cluster)
|
|
{
|
|
var x = (dayIndex * dayWidth) + (segment.ColumnIndex * subColumnWidth) + 2;
|
|
var width = Math.Max(0, subColumnWidth - 4);
|
|
var y = (segment.StartMinute / 60d) * hourHeight;
|
|
var height = Math.Max(1, ((segment.EndMinute - segment.StartMinute) / 60d) * hourHeight);
|
|
|
|
layouts.Add(new TimedItemLayout(segment.Item, dayIndex, date, new LayoutRect(x, y, width, height)));
|
|
}
|
|
}
|
|
}
|
|
|
|
return new TimedCalendarLayoutResult(visibleDates, dayWidth, layouts);
|
|
}
|
|
|
|
private static List<Segment> BuildDaySegments(IEnumerable<CalendarItemViewModel> items, DateOnly date)
|
|
{
|
|
var dayStart = date.ToDateTime(TimeOnly.MinValue);
|
|
var dayEnd = dayStart.AddDays(1);
|
|
var segments = new List<Segment>();
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (!CalendarItemAccessor.TryGetTimeRange(item, out var start, out var end))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var localStart = start.LocalDateTime;
|
|
var localEnd = end.LocalDateTime;
|
|
|
|
if (localEnd <= localStart)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var segmentStart = localStart > dayStart ? localStart : dayStart;
|
|
var segmentEnd = localEnd < dayEnd ? localEnd : dayEnd;
|
|
|
|
if (segmentEnd <= segmentStart)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
segments.Add(new Segment(item, (segmentStart - dayStart).TotalMinutes, (segmentEnd - dayStart).TotalMinutes));
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
private static IEnumerable<List<Segment>> BuildClusters(List<Segment> segments)
|
|
{
|
|
if (segments.Count == 0)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var cluster = new List<Segment> { segments[0] };
|
|
var clusterEnd = segments[0].EndMinute;
|
|
|
|
for (var index = 1; index < segments.Count; index++)
|
|
{
|
|
var segment = segments[index];
|
|
|
|
if (segment.StartMinute < clusterEnd)
|
|
{
|
|
cluster.Add(segment);
|
|
clusterEnd = Math.Max(clusterEnd, segment.EndMinute);
|
|
continue;
|
|
}
|
|
|
|
yield return cluster;
|
|
cluster = [segment];
|
|
clusterEnd = segment.EndMinute;
|
|
}
|
|
|
|
yield return cluster;
|
|
}
|
|
|
|
private static void AssignColumns(List<Segment> segments)
|
|
{
|
|
var columnEnds = new List<double>();
|
|
|
|
foreach (var segment in segments)
|
|
{
|
|
var assignedColumn = -1;
|
|
|
|
for (var columnIndex = 0; columnIndex < columnEnds.Count; columnIndex++)
|
|
{
|
|
if (columnEnds[columnIndex] <= segment.StartMinute)
|
|
{
|
|
assignedColumn = columnIndex;
|
|
columnEnds[columnIndex] = segment.EndMinute;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (assignedColumn < 0)
|
|
{
|
|
assignedColumn = columnEnds.Count;
|
|
columnEnds.Add(segment.EndMinute);
|
|
}
|
|
|
|
segment.ColumnIndex = assignedColumn;
|
|
}
|
|
}
|
|
|
|
private sealed record Segment(CalendarItemViewModel Item, double StartMinute, double EndMinute)
|
|
{
|
|
public int ColumnIndex { get; set; }
|
|
}
|
|
}
|