From d3911d5687f5ef7aa8cd915db1332da29465cc84 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 24 Dec 2021 14:40:54 +0100 Subject: [PATCH] Table: refactorization, cell placement algorithm --- QuestPDF.Examples/TableExamples.cs | 118 +++++++++++++----- .../Elements/Table/EnumerableExtensions.cs | 25 ++++ .../Elements/Table/ITableCellContainer.cs | 9 ++ QuestPDF/Elements/{ => Table}/Table.cs | 66 +--------- QuestPDF/Elements/Table/TableCell.cs | 11 ++ .../Table/TableCellRenderingCommand.cs | 11 ++ .../Elements/Table/TableColumnDefinition.cs | 16 +++ QuestPDF/Elements/Table/TableLayoutPlanner.cs | 82 ++++++++++++ QuestPDF/Elements/Table/TableRenderingPlan.cs | 12 ++ QuestPDF/Fluent/TableExtensions.cs | 6 +- 10 files changed, 259 insertions(+), 97 deletions(-) create mode 100644 QuestPDF/Elements/Table/EnumerableExtensions.cs create mode 100644 QuestPDF/Elements/Table/ITableCellContainer.cs rename QuestPDF/Elements/{ => Table}/Table.cs (77%) create mode 100644 QuestPDF/Elements/Table/TableCell.cs create mode 100644 QuestPDF/Elements/Table/TableCellRenderingCommand.cs create mode 100644 QuestPDF/Elements/Table/TableColumnDefinition.cs create mode 100644 QuestPDF/Elements/Table/TableLayoutPlanner.cs create mode 100644 QuestPDF/Elements/Table/TableRenderingPlan.cs diff --git a/QuestPDF.Examples/TableExamples.cs b/QuestPDF.Examples/TableExamples.cs index 4af7352..b7644af 100644 --- a/QuestPDF.Examples/TableExamples.cs +++ b/QuestPDF.Examples/TableExamples.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NUnit.Framework; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; @@ -9,7 +10,7 @@ namespace QuestPDF.Examples { public class TableExamples { - public static Random Random { get; } = new Random(0); + public static Random Random { get; } = new Random(); [Test] public void Example() @@ -24,8 +25,7 @@ namespace QuestPDF.Examples container .Padding(25) .Box() - .Border(2) - .MaxHeight(500) + .Border(2) .Table(table => { table.ColumnsDefinition(columns => @@ -36,40 +36,96 @@ namespace QuestPDF.Examples columns.ConstantColumn(200); }); - table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A")); - table.Cell().Row(1).Column(3).Element(CreateBox("B")); - table.Cell().Row(1).Column(4).Element(CreateBox("C")); + table.Cell().ColumnSpan(2).Element(CreateBox("A")); + table.Cell().Element(CreateBox("B")); + table.Cell().Element(CreateBox("C")); - table.Cell().Row(2).Column(1).Element(CreateBox("D")); - table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E")); - table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F")); + table.Cell().Element(CreateBox("D")); + table.Cell().RowSpan(2).Element(CreateBox("E")); + table.Cell().RowSpan(3).ColumnSpan(2).Element(CreateBox("F")); - table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G")); - table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H")); - table.Cell().Row(5).Column(3).Element(CreateBox("I")); - table.Cell().Row(5).Column(4).Element(CreateBox("J")); - table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K")); - table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L")); - table.Cell().Row(6).Column(4).Element(CreateBox("M")); + table.Cell().RowSpan(2).Element(CreateBox("G")); + table.Cell().RowSpan(2).Element(CreateBox("H")); + table.Cell().Element(CreateBox("I")); + table.Cell().Element(CreateBox("J")); + table.Cell().RowSpan(2).Element(CreateBox("K")); + table.Cell().ColumnSpan(2).Element(CreateBox("L")); + table.Cell().Element(CreateBox("M")); + + // table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A")); + // table.Cell().Row(1).Column(3).Element(CreateBox("B")); + // table.Cell().Row(1).Column(4).Element(CreateBox("C")); + // + // table.Cell().Row(2).Column(1).Element(CreateBox("D")); + // table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E")); + // table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F")); + // + // table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G")); + // table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H")); + // table.Cell().Row(5).Column(3).Element(CreateBox("I")); + // table.Cell().Row(5).Column(4).Element(CreateBox("J")); + // table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K")); + // table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L")); + // table.Cell().Row(6).Column(4).Element(CreateBox("M")); }); }); - - Action CreateBox(string label) - { - return container => + } + + [Test] + public void PerformanceTest() + { + RenderingTest + .Create() + .ProducePdf() + .PageSize(1000, 2000) + .MaxPages(1000) + .ShowResults() + .Render(container => { - var height = Random.Next(2, 7) * 25; - container - .Border(1) - .Background(Placeholders.BackgroundColor()) - .AlignCenter() - .AlignMiddle() - .Border(1) - .MinHeight(height) - .Text($"{label}: {height}"); - }; - } + .Table(table => + { + table.ColumnsDefinition(columns => + { + foreach (var size in Enumerable.Range(0, 10)) + columns.ConstantColumn(100); + }); + + foreach (var i in Enumerable.Range(1, 10000)) + { + table + .Cell() + .RowSpan((uint)Random.Next(1, 5)) + .ColumnSpan((uint)Random.Next(1, 5)) + .Element(CreateBox(i.ToString())); + } + }); + }); + } + + private Action CreateBox(string label) + { + return container => + { + var height = Random.Next(2, 7) * 25; + + container + .Border(1) + .Background(Placeholders.BackgroundColor()) + .Layers(layers => + { + layers + .PrimaryLayer() + .ExtendHorizontal() + .Height(height); + + layers + .Layer() + .AlignCenter() + .AlignMiddle() + .Text($"{label}: {height}px"); + }); + }; } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Table/EnumerableExtensions.cs b/QuestPDF/Elements/Table/EnumerableExtensions.cs new file mode 100644 index 0000000..ef6de35 --- /dev/null +++ b/QuestPDF/Elements/Table/EnumerableExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace QuestPDF.Elements.Table +{ + internal static class EnumerableExtensions + { + public static IEnumerable Scan(this IEnumerable input, Func accumulate) + { + using var enumerator = input.GetEnumerator(); + + if (!enumerator.MoveNext()) + yield break; + + var state = enumerator.Current; + yield return state; + + while (enumerator.MoveNext()) + { + state = accumulate(state, enumerator.Current); + yield return state; + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/ITableCellContainer.cs b/QuestPDF/Elements/Table/ITableCellContainer.cs new file mode 100644 index 0000000..fb02e24 --- /dev/null +++ b/QuestPDF/Elements/Table/ITableCellContainer.cs @@ -0,0 +1,9 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements.Table +{ + public interface ITableCellContainer : IContainer + { + + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table.cs b/QuestPDF/Elements/Table/Table.cs similarity index 77% rename from QuestPDF/Elements/Table.cs rename to QuestPDF/Elements/Table/Table.cs index 83d7b9a..a38bb7d 100644 --- a/QuestPDF/Elements/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -1,55 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using QuestPDF.Drawing; -using QuestPDF.Fluent; using QuestPDF.Infrastructure; -namespace QuestPDF.Elements +namespace QuestPDF.Elements.Table { - internal class TableColumnDefinition - { - public float ConstantSize { get; } - public float RelativeSize { get; } - - internal float Width { get; set; } - - public TableColumnDefinition(float constantSize, float relativeSize) - { - ConstantSize = constantSize; - RelativeSize = relativeSize; - } - } - - public interface ITableCellContainer : IContainer - { - - } - - internal class TableCell : Container, ITableCellContainer - { - public int Row { get; set; } = 1; - public int RowSpan { get; set; } = 1; - - public int Column { get; set; } = 1; - public int ColumnSpan { get; set; } = 1; - } - - internal class TableRenderingPlan - { - public Size Size { get; set; } - public List CellRenderingCommands { get; set; } - public int MaxRowRendered { get; set; } - } - - internal class TableCellRenderingCommand - { - public TableCell Cell { get; set; } - public Size Size { get; set; } - public Position Offset { get; set; } - } - internal class Table : Element, IStateResettable { public ICollection Columns { get; } = new List(); @@ -212,24 +168,4 @@ namespace QuestPDF.Elements return Children.Max(x => x.Row + x.RowSpan); } } - - internal static class EnumerableExtensions - { - public static IEnumerable Scan(this IEnumerable input, Func accumulate) - { - using var enumerator = input.GetEnumerator(); - - if (!enumerator.MoveNext()) - yield break; - - var state = enumerator.Current; - yield return state; - - while (enumerator.MoveNext()) - { - state = accumulate(state, enumerator.Current); - yield return state; - } - } - } } \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableCell.cs b/QuestPDF/Elements/Table/TableCell.cs new file mode 100644 index 0000000..2b8cc5c --- /dev/null +++ b/QuestPDF/Elements/Table/TableCell.cs @@ -0,0 +1,11 @@ +namespace QuestPDF.Elements.Table +{ + internal class TableCell : Container, ITableCellContainer + { + public int Row { get; set; } = 1; + public int RowSpan { get; set; } = 1; + + public int Column { get; set; } = 1; + public int ColumnSpan { get; set; } = 1; + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableCellRenderingCommand.cs b/QuestPDF/Elements/Table/TableCellRenderingCommand.cs new file mode 100644 index 0000000..b74114e --- /dev/null +++ b/QuestPDF/Elements/Table/TableCellRenderingCommand.cs @@ -0,0 +1,11 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements.Table +{ + internal class TableCellRenderingCommand + { + public TableCell Cell { get; set; } + public Size Size { get; set; } + public Position Offset { get; set; } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableColumnDefinition.cs b/QuestPDF/Elements/Table/TableColumnDefinition.cs new file mode 100644 index 0000000..516c6ed --- /dev/null +++ b/QuestPDF/Elements/Table/TableColumnDefinition.cs @@ -0,0 +1,16 @@ +namespace QuestPDF.Elements.Table +{ + internal class TableColumnDefinition + { + public float ConstantSize { get; } + public float RelativeSize { get; } + + internal float Width { get; set; } + + public TableColumnDefinition(float constantSize, float relativeSize) + { + ConstantSize = constantSize; + RelativeSize = relativeSize; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableLayoutPlanner.cs b/QuestPDF/Elements/Table/TableLayoutPlanner.cs new file mode 100644 index 0000000..131a889 --- /dev/null +++ b/QuestPDF/Elements/Table/TableLayoutPlanner.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Fluent; + +namespace QuestPDF.Elements.Table +{ + static class TableLayoutPlanner + { + public static void PlanCellPositions(this Table table) + { + PlanCellPositions(table.Columns.Count, table.Children); + } + + private static void PlanCellPositions(int columnsCount, ICollection cells) + { + var cellsWindow = new List(); + (int x, int y) currentLocation = (1, 1); + + foreach (var cell in cells) + { + if (cellsWindow.Count > Math.Max(columnsCount, 16)) + { + cellsWindow = cellsWindow + .Where(x => x.Row + x.RowSpan > currentLocation.y) + .ToList(); + } + + if (cell.HasLocation()) + { + cellsWindow.Add(cell); + currentLocation = (cell.Column, cell.Row); + continue; + } + + foreach (var location in GenerateCoordinates(columnsCount, currentLocation)) + { + cell.Column = location.x; + cell.Row = location.y; + + if (cell.CollidesWithAnyOf(cellsWindow)) + continue; + + cellsWindow.Add(cell); + currentLocation = (cell.Column, cell.Row); + break; + } + } + } + + private static IEnumerable<(int x, int y)> GenerateCoordinates(int columnsCount, (int x, int y) startPosition) + { + if (startPosition.x > columnsCount) + throw new ArgumentException(); + + foreach (var x in Enumerable.Range(startPosition.x, columnsCount - startPosition.x + 1)) + yield return (x, startPosition.y); + + foreach (var y in Enumerable.Range(startPosition.y + 1, 1_000_000)) + foreach (var x in Enumerable.Range(1, columnsCount)) + yield return (x, y); + } + + private static bool CollidesWith(this TableCell cell, TableCell neighbour) + { + return cell.Column < neighbour.Column + neighbour.ColumnSpan && + cell.Column + cell.ColumnSpan > neighbour.Column && + cell.Row < neighbour.Row + neighbour.RowSpan && + cell.RowSpan + cell.Row > neighbour.Row; + } + + private static bool CollidesWithAnyOf(this TableCell cell, ICollection neighbours) + { + return neighbours.Any(cell.CollidesWith); + } + + private static bool HasLocation(this TableCell cell) + { + return cell.Row != 1 && cell.Column != 1; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableRenderingPlan.cs b/QuestPDF/Elements/Table/TableRenderingPlan.cs new file mode 100644 index 0000000..009da09 --- /dev/null +++ b/QuestPDF/Elements/Table/TableRenderingPlan.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements.Table +{ + internal class TableRenderingPlan + { + public Size Size { get; set; } + public List CellRenderingCommands { get; set; } + public int MaxRowRendered { get; set; } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/TableExtensions.cs b/QuestPDF/Fluent/TableExtensions.cs index 7ec350b..5b48d7e 100644 --- a/QuestPDF/Fluent/TableExtensions.cs +++ b/QuestPDF/Fluent/TableExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using QuestPDF.Elements; +using QuestPDF.Elements.Table; using QuestPDF.Infrastructure; namespace QuestPDF.Fluent @@ -50,7 +52,7 @@ namespace QuestPDF.Fluent Table.Spacing = value; } - public ITableCellContainer Cell() + public ITableCellContainer Cell(int row = 1, int column = 1, int rowSpan = 1, int columnsSpan = 1) { var cell = new TableCell(); Table.Children.Add(cell); @@ -66,6 +68,8 @@ namespace QuestPDF.Fluent var descriptor = new TableDescriptor(table); handler(descriptor); + table.PlanCellPositions(); + element.Element(table); } }