tbf/Results/Output/Table.cs

573 lines
24 KiB
C#

///
/// Copyright (c) 2018-2023 Sensus Slovensko a.s.
///
using Common;
using Results.Resources;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
namespace Results.Output
{
public enum TableLines
{
None,
FirstAndLast,
FirstSecondAndLast,
All,
}
public class TableStyle
{
public float MarginX;
public float MarginY;
public Font Font;
public Font HeaderFont;
public int TopHeadersCount;
public int LeftHeadersCount;
public float LineWidth;
public TableLines HorizLines;
public TableLines VertLines;
public float[] ColumnWidths; /// null = automatic
}
public class Table
{
public readonly int ColumnsCount; /// Number of columns including the headers
public readonly string AutoTestParam; /// Reserved for future use
public readonly IList<Cell[]> Cells; /// Table content: List of rows, each row is an array of cells
public readonly TableStyle Style;
/// <summary>
/// Calculated by Measure(e)
/// </summary>
float width;
float height;
float[] columnWidth; /// Widths of columns of cells
float[] columnPos; /// X-coordinate of positions of texts in columns (MarginX is left empty)
float[] vertLinePos; /// X-coordinate of positions of vertical lines
float[] rowHeight; /// Heights of rows of cells
float[] rowPos; /// Y-coordinate of positions of texts in columns (MarginY is left empty)
float[] horizLinePos; /// Y-coordinate of positions of horizontal lines
public int RowsCount /// Number of rows including the headers
{
get { return Cells.Count; }
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="columnsCount">Number of columns</param>
/// <param name="autoTestParam">Reserved for future use</param>
public Table(int columnsCount, TableStyle style, string autoTestParam = null)
{
this.ColumnsCount = columnsCount;
this.Style = style;
this.AutoTestParam = autoTestParam;
Cells = new List<Cell[]>();
columnWidth = null;
}
public void Clear()
{
Cells.Clear();
columnWidth = null;
}
public void AddRow(Cell[] rowOfCells)
{
if (rowOfCells != null && rowOfCells.Length == ColumnsCount)
{
Cells.Add(rowOfCells);
}
}
/// <summary>
/// Returns true when table has either rows or columns count == 0
/// </summary>
/// <returns>true when empty</returns>
public bool IsEmpty()
{
return RowsCount == 0 || ColumnsCount == 0;
}
/// <summary>
/// Calculates table dimensions. Inputs: ColumnsCount, Cells, Style
/// </summary>
/// <param name="e">PrintPageEventArgs passed to Measure(...)</param>
/// <returns>Table width and height (SizeF)</returns>
public SizeF Measure(PrintPageEventArgs e)
{
if (IsEmpty()) return new SizeF(0, 0); /// Empty table => zero Size
columnWidth = new float[ColumnsCount];
columnPos = new float[ColumnsCount];
vertLinePos = new float[ColumnsCount + 1];
rowHeight = new float[RowsCount];
rowPos = new float[RowsCount];
horizLinePos = new float[RowsCount + 1];
width = 0;
height = 0;
vertLinePos[0] = 0;
horizLinePos[0] = 0;
///
/// Measure table column widths, vertical line and text positions, etc.
///
for (int colIx = 0; colIx < ColumnsCount; colIx++)
{
bool hasLineToLeft = false;
for (int rowIx = 0; rowIx < RowsCount; rowIx++)
{
Cells[rowIx][colIx].Measure(e);
float cellWidth = (Style.ColumnWidths != null && Style.ColumnWidths.Length > colIx)
? Style.ColumnWidths[colIx]
: Cells[rowIx][colIx].Size.Width;
if (cellWidth > columnWidth[colIx]) columnWidth[colIx] = cellWidth;
if (Cells[rowIx][colIx].LeftBorder) hasLineToLeft = true;
}
width += columnWidth[colIx] + (hasLineToLeft ? (Style.LineWidth + Style.MarginX) : 0) + Style.MarginX;
columnPos[colIx] = vertLinePos[colIx] + (hasLineToLeft ? (Style.LineWidth + Style.MarginX) : 0);
vertLinePos[colIx + 1] = columnPos[colIx] + columnWidth[colIx] + Style.MarginX;
}
///
/// Measure table row heights, horixontal line and text positions, etc.
///
for (int rowIx = 0; rowIx < RowsCount; rowIx++)
{
bool hasLineAbove = false;
for (int colIx = 0; colIx < ColumnsCount; colIx++)
{
if (Cells[rowIx][colIx].Size.Height > rowHeight[rowIx]) rowHeight[rowIx] = Cells[rowIx][colIx].Size.Height;
if (Cells[rowIx][colIx].TopBorder) hasLineAbove = true;
}
height += rowHeight[rowIx] + (hasLineAbove ? (Style.LineWidth + Style.MarginY) : 0) + Style.MarginY;
rowPos[rowIx] = horizLinePos[rowIx] + (hasLineAbove ? Style.MarginY : 0);
horizLinePos[rowIx + 1] = rowPos[rowIx] + rowHeight[rowIx] + Style.LineWidth + Style.MarginY;
}
if (Style.VertLines == TableLines.None) width -= Style.MarginX;
if (Style.HorizLines == TableLines.None) height -= Style.MarginY;
return new SizeF(width, height);
}
/// <summary>
/// Prints a table defined by ColumnsCount, Rows, HorizAlignments, VertAlignments and Style
/// </summary>
/// <param name="e">PrintPageEventArgs passed to Measure(.), DrawLine(.) and PrintAt(.)</param>
/// <param name="left">X-coordinate of the top-left corner of the table</param>
/// <param name="top">Y-coordinate of the top-left corner of the table</param>
/// <returns>Y-coordinate for a next item following this table on a page</returns>
public float Draw(PrintPageEventArgs e, float left, float top)
{
if (IsEmpty()) return top; /// Empty table or Mesure(e) have not been called yet
if (columnWidth == null) Measure(e);
for (int rowIx = 0; rowIx <= RowsCount; rowIx++)
{
for (int colIx = 0; colIx <= ColumnsCount; colIx++)
{
///
/// Draw horizontal line
///
if (colIx < ColumnsCount && Style.HorizLines != TableLines.None &&
(rowIx == 0 || rowIx == RowsCount || Cells[rowIx][colIx].TopBorder))
{
e.Graphics.DrawLine(new Pen(Color.Black, Style.LineWidth),
new PointF(left + vertLinePos[colIx], top + horizLinePos[rowIx] + Style.LineWidth / 2),
new PointF(left + vertLinePos[colIx + 1] + Style.LineWidth, top + horizLinePos[rowIx] + Style.LineWidth / 2));
}
///
/// Draw vertical line
///
if (rowIx < RowsCount && Style.VertLines != TableLines.None &&
(colIx == 0 || colIx == ColumnsCount || Cells[rowIx][colIx].LeftBorder))
{
e.Graphics.DrawLine(new Pen(Color.Black, Style.LineWidth),
new PointF(left + vertLinePos[colIx] + Style.LineWidth / 2, top + horizLinePos[rowIx]),
new PointF(left + vertLinePos[colIx] + Style.LineWidth / 2, top + horizLinePos[rowIx + 1] + Style.LineWidth));
}
///
/// Draw table item (a text)
///
if (rowIx < RowsCount && colIx < ColumnsCount) /// Make sure Cells[][] indices are within range
{
if (!Cells[rowIx][colIx].BottomMerge && !Cells[rowIx][colIx].RightMerge)
{
/// This is not a cell that is merged with a subsequent cell => print content
/// Calculate X-coordinate taking into account cell merging
float mergedColPos = columnPos[colIx];
int mergedCellsCount = 1;
for (int colIx2 = colIx - 1; colIx2 >= 0 && Cells[rowIx][colIx2].RightMerge; colIx2--)
{
mergedColPos += columnPos[colIx2];
mergedCellsCount++;
}
mergedColPos /= mergedCellsCount;
/// Calculate Y-coordinate taking into account cell merging
float mergedRowPos = rowPos[rowIx];
mergedCellsCount = 1;
List<string> texts = new List<string>();
texts.Add(Cells[rowIx][colIx].Text);
for (int rowIx2 = rowIx - 1; rowIx2 >= 0 && Cells[rowIx2][colIx].BottomMerge; rowIx2--)
{
mergedRowPos += rowPos[rowIx2];
mergedCellsCount++;
texts.Add(Cells[rowIx2][colIx].Text);
}
mergedRowPos /= mergedCellsCount;
/// Print the text at the calculated position
Common.Printers.PrintersCommon.PrintAt(e, mergedCellsCount == 1 ? Cells[rowIx][colIx].Text : GetMergedCellStringAsNegativeOrPositive(texts),
Cells[rowIx][colIx].Font,
left + mergedColPos,
top + mergedRowPos,
columnWidth[colIx],
rowHeight[rowIx],
Cells[rowIx][colIx].HorizAlignment,
Cells[rowIx][colIx].VertAlignment);
}
}
}
}
return top + horizLinePos[RowsCount] + Style.LineWidth;
}
public static Table Create_TestsAreRows(Entities.WaterMeter wm, IList<WMeterRsltItemSpec> items, TableStyle style, string autoTestParam = null)
{
/// Select results to be printed
var mtrTestRslts = string.IsNullOrEmpty(autoTestParam) ? wm.RegularMeterTestRslts() : wm.AutoMeterTestRslts(autoTestParam);
///
/// Calculate rows count
///
int rowsCount = 1; /// Top row contains item captions
foreach (var mtr in mtrTestRslts)
{
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
rowsCount++; /// One additional row for each test to be published
}
}
Table table = new Table(items.Count, style);
///
/// Top row contains column captions
///
var header = new Cell[items.Count];
for (int i = 0; i < items.Count; i++)
{
header[i] = new Cell(items[i].Caption, style.HeaderFont, Alignment.Center, VertAlignment.Middle, true, true);
}
table.AddRow(header);
///
/// Create and add a row for each test
///
WMeterRsltItemSpec.TableRowNr = 1;
foreach (var mtr in mtrTestRslts)
{
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
var aTableRow = new Cell[items.Count];
for (int i = 0; i < items.Count; i++)
{
WMeterRsltItemSpec.TableColumnNr = i + 1;
string text = items[i].Print(wm, mtr.Name()).Split(new char[] { '|' })[0];
aTableRow[i] = new Cell(text,
style.Font,
items[i].Alignment,
VertAlignment.Middle,
(WMeterRsltItemSpec.TableRowNr == 1) || !items[i].Merge,
true,
(WMeterRsltItemSpec.TableRowNr != rowsCount - 1) && items[i].Merge,
false);
}
table.AddRow(aTableRow);
WMeterRsltItemSpec.TableRowNr++;
}
}
return table;
}
public string GetMergedCellStringAsNegativeOrPositive(List<string> texts)
{
if (texts.Count < 1)
{
return "";
}
bool firstIsHead = false;
bool hasNegativeAll = true;
bool hasPositiveAll = true;
bool hasMixed = false;
string lastRowText = null;
//check if is a header
foreach (string text in texts)
{
if (text != "-")
{
hasNegativeAll = false;
}
if (text != "+")
{
hasPositiveAll = false;
}
if (!hasMixed)
{
if (lastRowText == null)
{
lastRowText = text;
}
else
{
if (lastRowText != text)
{
hasMixed = true;
}
lastRowText = text;
}
}
}
if (hasMixed)
{
//merge as negative
return "-";
}
else if (hasPositiveAll)
{
//merge as positive
return "+";
}
else if (hasNegativeAll)
{
//merge as negative
return "-";
}
return texts[0];
}
/// <summary>
/// Create a table where tests are in columns.
/// Left column contains item (or row) captions.
/// </summary>
/// <param name="wm">Water meter</param>
/// <param name="items">Result items</param>
/// <param name="style">Table style</param>
/// <param name="autoTestParam">Auto test parameter</param>
/// <returns>Table to be printed</returns>
public static Table Create_TestsAreColumns(Entities.WaterMeter wm, IList<WMeterRsltItemSpec> items, TableStyle style, string autoTestParam = null)
{
/// Select results to be printed
var mtrTestRslts = string.IsNullOrEmpty(autoTestParam) ? wm.RegularMeterTestRslts() : wm.AutoMeterTestRslts(autoTestParam);
///
/// Calculate columns count
///
int columnsCount = 1; /// Left column contains item captions
foreach (var mtr in mtrTestRslts)
{
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
columnsCount++; /// One additional column for each test to be published
}
}
Table table = new Table(columnsCount, style);
///
/// Table content
///
bool separatorLineSignalled = false;
WMeterRsltItemSpec.TableRowNr = 1;
for (int i = 0; i < items.Count; i++)
{
if (items[i].Uid == (int)ItemID.Separator)
{
/// Insert a separator line (topBorder = true for all row cells)
separatorLineSignalled = true;
continue;
}
bool topBorder = (style.HorizLines == TableLines.All)
|| (style.HorizLines == TableLines.FirstAndLast && i == 0)
|| (style.HorizLines == TableLines.FirstSecondAndLast && i <= 1)
|| separatorLineSignalled;
separatorLineSignalled = false;
Cell[] aTableRow = new Cell[columnsCount];
aTableRow[0] = new Cell(items[i].Caption,
style.HeaderFont,
Alignment.Center,
VertAlignment.Middle,
topBorder,
style.VertLines != TableLines.None);
WMeterRsltItemSpec.TableColumnNr = 1;
for (int j = 0; j < mtrTestRslts.Count; j++)
{
var mtr = mtrTestRslts[j];
bool leftBorder = (style.VertLines == TableLines.All && (!items[i].Merge || j == 0))
|| (style.VertLines == TableLines.FirstSecondAndLast && j == 0);
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
/// Get a text, strip color information
string text = items[i].Print(wm, mtr.Name()).Split(new char[] { '|' })[0];
aTableRow[WMeterRsltItemSpec.TableColumnNr] = new Cell(text,
style.Font,
items[i].Alignment,
VertAlignment.Middle,
topBorder,
leftBorder,
false,
(WMeterRsltItemSpec.TableColumnNr + 1 != columnsCount) && items[i].Merge);
WMeterRsltItemSpec.TableColumnNr++;
}
}
table.AddRow(aTableRow);
WMeterRsltItemSpec.TableRowNr++;
}
return table;
}
/// <summary>
/// Create a table where tests are in columns.
/// Two leftmost columns contains item (or row) captions and units.
/// Separator lines can be inserted between rows.
/// </summary>
/// <param name="wm">Water meter</param>
/// <param name="items">Result items</param>
/// <param name="style">Table style</param>
/// <param name="autoTestParam">Auto test parameter</param>
/// <returns>Table to be printed</returns>
public static Table Create_TestsAreColumnsPL(Entities.WaterMeter wm, IList<WMeterRsltItemSpec> items, TableStyle style, string autoTestParam = null)
{
/// Select results to be printed
var mtrTestRslts = string.IsNullOrEmpty(autoTestParam) ? wm.RegularMeterTestRslts() : wm.AutoMeterTestRslts(autoTestParam);
///
/// Calculate columns count
///
int columnsCount = 2; /// Two left columns contain item caption and unit
foreach (var mtr in mtrTestRslts)
{
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
columnsCount++; /// One additional column for each test to be published
}
}
Table table = new Table(columnsCount, style);
///
/// Table content
///
bool separatorLineSignalled = false;
WMeterRsltItemSpec.TableRowNr = 1;
for (int i = 0; i < items.Count; i++)
{
if (items[i].Uid == (int)ItemID.Separator)
{
/// Insert a separator line (topBorder = true for all row cells)
separatorLineSignalled = true;
continue;
}
bool topBorder = (style.HorizLines == TableLines.All)
|| (style.HorizLines == TableLines.FirstAndLast && i == 0)
|| (style.HorizLines == TableLines.FirstSecondAndLast && i <= 1)
|| separatorLineSignalled;
separatorLineSignalled = false;
Cell[] aTableRow = new Cell[columnsCount];
aTableRow[0] = new Cell(items[i].Caption,
style.HeaderFont,
Alignment.Center,
VertAlignment.Middle,
topBorder,
style.VertLines != TableLines.None);
aTableRow[1] = new Cell((i == 0) ? Strings.Unit : items[i].Units.ToDescription(),
style.HeaderFont,
Alignment.Center,
VertAlignment.Middle,
topBorder,
style.VertLines == TableLines.All || style.VertLines == TableLines.FirstSecondAndLast);
WMeterRsltItemSpec.TableColumnNr = 1;
for (int j = 0; j < mtrTestRslts.Count; j++)
{
var mtr = mtrTestRslts[j];
bool leftBorder = (style.VertLines == TableLines.All && (!items[i].Merge || j == 0))
|| (style.VertLines == TableLines.FirstSecondAndLast && WMeterRsltItemSpec.TableColumnNr == 1);
if (mtr != null && mtr.IsPilotRslt() && mtr.Publish() == Publish.Always)
{
/// Get a text, strip color information
string text = items[i].Print(wm, mtr.Name()).Split(new char[] { '|' })[0];
aTableRow[WMeterRsltItemSpec.TableColumnNr + 1] = new Cell(text,
style.Font,
items[i].Alignment,
VertAlignment.Middle,
topBorder,
leftBorder,
false,
(WMeterRsltItemSpec.TableColumnNr + 2 != columnsCount) && items[i].Merge);
WMeterRsltItemSpec.TableColumnNr++;
}
}
table.AddRow(aTableRow);
WMeterRsltItemSpec.TableRowNr++;
}
return table;
}
}
}