Forráskód Böngészése

Table: refactorization, cell placement algorithm

MarcinZiabek 4 éve
szülő
commit
d3911d5687

+ 86 - 30
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"));
                         });
                 });
+        }
+        
+        [Test]
+        public void PerformanceTest()
+        {
+            RenderingTest
+                .Create()
+                .ProducePdf()
+                .PageSize(1000, 2000)
+                .MaxPages(1000)
+                .ShowResults()
+                .Render(container =>
+                {
+                    container
+                        .Table(table =>
+                        {
+                            table.ColumnsDefinition(columns =>
+                            {
+                                foreach (var size in Enumerable.Range(0, 10))
+                                    columns.ConstantColumn(100);
+                            });
 
-            Action<IContainer> CreateBox(string label)
+                            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<IContainer> CreateBox(string label)
+        {
+            return container =>
             {
-                return container =>
-                {
-                    var height = Random.Next(2, 7) * 25;
+                var height = Random.Next(2, 7) * 25;
                     
-                    container
-                        .Border(1)
-                        .Background(Placeholders.BackgroundColor())
-                        .AlignCenter()
-                        .AlignMiddle()
-                        .Border(1)
-                        .MinHeight(height)
-                        .Text($"{label}: {height}");
-                };
-            }
+                container
+                    .Border(1)
+                    .Background(Placeholders.BackgroundColor())
+                    .Layers(layers =>
+                    {
+                        layers
+                            .PrimaryLayer()
+                            .ExtendHorizontal()
+                            .Height(height);
+                            
+                        layers
+                            .Layer()
+                            .AlignCenter()
+                            .AlignMiddle()
+                            .Text($"{label}: {height}px");
+                    });
+            };
         }
     }
 }

+ 25 - 0
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<T> Scan<T>(this IEnumerable<T> input, Func<T, T, T> 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;
+            }
+        }
+    }
+}

+ 9 - 0
QuestPDF/Elements/Table/ITableCellContainer.cs

@@ -0,0 +1,9 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements.Table
+{
+    public interface ITableCellContainer : IContainer
+    {
+            
+    }
+}

+ 1 - 65
QuestPDF/Elements/Table.cs → 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<TableCellRenderingCommand> 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<TableColumnDefinition> Columns { get; } = new List<TableColumnDefinition>();
@@ -212,24 +168,4 @@ namespace QuestPDF.Elements
             return Children.Max(x => x.Row + x.RowSpan);
         }
     }
-    
-    internal static class EnumerableExtensions
-    {
-        public static IEnumerable<T> Scan<T>(this IEnumerable<T> input, Func<T, T, T> 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;
-            }
-        }
-    }
 }

+ 11 - 0
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;
+    }
+}

+ 11 - 0
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; }
+    }
+}

+ 16 - 0
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;
+        }
+    }
+}

+ 82 - 0
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<TableCell> cells)
+        {
+            var cellsWindow = new List<TableCell>();
+            (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<TableCell> neighbours)
+        {
+            return neighbours.Any(cell.CollidesWith);
+        }
+
+        private static bool HasLocation(this TableCell cell)
+        {
+            return cell.Row != 1 && cell.Column != 1;
+        }
+    }
+}

+ 12 - 0
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<TableCellRenderingCommand> CellRenderingCommands { get; set; }
+        public int MaxRowRendered { get; set; }
+    }
+}

+ 5 - 1
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);
         }
     }