devexpress-gantt
Version:
DevExpress Gantt Control
483 lines (474 loc) • 22.9 kB
text/typescript
import { Point } from "@devexpress/utils/lib/geometry/point";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { PdfDependencyLineInfo } from "../DataObjects/DependencyLineInfo";
import { PdfTaskInfo } from "../DataObjects/TaskInfo";
import { PdfTaskResourcesInfo } from "../DataObjects/TaskResourcesInfo";
import { PdfTimeMarkerInfo } from "../DataObjects/TimeMarkerInfo";
import { IGanttPageExportInfo, IGanttTableExportInfo, PdfPageTableNames } from "../Interfaces";
import { GanttPdfExportProps } from "../Settings/Props";
import { CellDef } from "../Table/CellDef";
import { PageNavigation } from "./PageNavigation";
class CellNavigationInfo {
pageVerIndex: number;
pageHorIndex: number;
cellRowIndexOnPage: number;
cellColIndexOnPage: number;
cell: CellDef;
constructor(pageHorIndex: number, pageVerIndex: number, cellRowIndexOnPage: number, cellColIndexOnPage: number, cell: CellDef) {
this.pageVerIndex = pageVerIndex;
this.pageHorIndex = pageHorIndex;
this.cellRowIndexOnPage = cellRowIndexOnPage;
this.cellColIndexOnPage = cellColIndexOnPage;
this.cell = cell;
}
}
class VectorInfo {
pageIndex: number;
globalCellIndex: number;
pageOffset: number;
cutSize: number;
constructor(pageIndex: number, globalCellIndex: number, pageOffset: number, cutSize?: number) {
this.pageIndex = pageIndex;
this.globalCellIndex = globalCellIndex;
this.pageOffset = pageOffset;
this.cutSize = cutSize;
}
public get isCutted(): boolean {
return this.cutSize > 0;
}
public get cellIndexOnPage(): number {
return this.globalCellIndex - this.pageOffset;
}
}
export class PdfGanttPaginator {
private _pdfDoc;
private _props: GanttPdfExportProps;
private _globalInfo: IGanttPageExportInfo;
private _pages: Array<Array<IGanttPageExportInfo>>;
private _pageNavigator: PageNavigation;
private _correctedPageBottoms: Array<number>;
private _pageLeft: number;
private _pageRight: number;
private _pageTop: number;
private _pageBottom: number;
constructor(pdfDoc: any, props: GanttPdfExportProps, globalInfo: IGanttPageExportInfo) {
this._pdfDoc = pdfDoc;
this._props = props;
this._globalInfo = globalInfo;
}
public getPages(): Array<IGanttPageExportInfo> {
delete this._pages;
this._paginateTables();
this._paginateObjects();
return this.pageMatrixToArray;
}
private _paginateTables(): void {
this._paginateTable(PdfPageTableNames.treeListHeader);
this._paginateTable(PdfPageTableNames.treeListMain);
this._paginateTable(PdfPageTableNames.chartScaleBottom);
this._paginateTable(PdfPageTableNames.chartScaleTop);
this._paginateTable(PdfPageTableNames.chartMain);
}
private _paginateObjects(): void {
this._paginateTasks();
this._paginateDependencies();
this._paginateResources();
this._paginateTimeMarkers();
}
private get pageMatrixToArray(): Array<IGanttPageExportInfo> {
let result = new Array<IGanttPageExportInfo>();
this._pages?.forEach(row => {
result = result.concat(row);
});
return result;
}
private _paginateTasks(): void {
this._globalInfo.objects?.tasks?.forEach(t => this._paginateTask(t));
}
private _paginateDependencies(): void {
this._globalInfo.objects?.dependencies?.forEach(d => {
if(d.arrowInfo)
this._paginateArrow(d);
else
this._paginateDependencyLine(d);
});
}
private _paginateResources(): void {
this._globalInfo.objects?.resources?.forEach(r => this._paginateResource(r));
}
private _paginateTimeMarkers(): void {
this._globalInfo.objects?.timeMarkers?.forEach(m => this._paginateTimeMarker(m));
}
private _paginateTable(tableKey: string): void {
const table = this._globalInfo?.tables[tableKey];
if(table) {
const start = this._getTableStart(table);
const matrix = this._preparePagesNavigationMatrixForTable(start, table);
const rowCount = matrix.length;
for(let i = 0; i < rowCount; i++) {
const colCount = matrix[i].length;
for(let j = 0; j < colCount; j++) {
const navInfo = matrix[i][j];
const page = this._getPage(navInfo.pageVerIndex, navInfo.pageHorIndex, true);
const tablePositionX = navInfo.pageHorIndex === start.hIndex ? start.pageX : this.pageLeft;
const tablePositionY = navInfo.pageVerIndex === start.vIndex ? start.pageY : this.pageTop;
this._setTablePositionOnPage(page, tableKey, tablePositionX, tablePositionY);
this._addCellToPage(page, tableKey, navInfo);
}
}
this._updateTableSizeOnPages(tableKey);
}
}
private _paginateTask(task: PdfTaskInfo): void {
const hOffsets = this._getTaskPagination(task);
const vOffsets = this._getTaskPagination(task, true);
for(let i = 0; i < vOffsets.length; i++)
for(let j = 0; j < hOffsets.length; j++) {
const newTask = new PdfTaskInfo();
newTask.assign(task);
this._offsetPoints(newTask.sidePoints, hOffsets[j].offset, vOffsets[i].offset);
this._addTaskToPage(vOffsets[i].pageIndex, hOffsets[j].pageIndex, newTask);
}
}
private _paginateArrow(dependency: PdfDependencyLineInfo): void {
const pointInfo = this._getPointPageInfo(dependency.points[0]);
const newDependency = new PdfDependencyLineInfo();
newDependency.assign(dependency);
this._offsetPoints(newDependency.points, pointInfo.offsetX, pointInfo.offsetY);
this._addDependencyToPage(pointInfo.pageVerIndex, pointInfo.pageHorIndex, newDependency);
}
private _paginateDependencyLine(dependency: PdfDependencyLineInfo): void {
const hPagination = this._getDependencyLinePagination(dependency);
const vPagination = this._getDependencyLinePagination(dependency, true);
for(let i = 0; i < vPagination.length; i++)
for(let j = 0; j < hPagination.length; j++) {
const newDependency = new PdfDependencyLineInfo();
newDependency.assign(dependency);
this._offsetPoints(newDependency.points, hPagination[j].offset, vPagination[i].offset);
this._addDependencyToPage(vPagination[i].pageIndex, hPagination[j].pageIndex, newDependency);
}
}
private _paginateResource(resource: PdfTaskResourcesInfo): void {
const pointInfo = this._getPointPageInfo(new Point(resource.x, resource.y));
const newResource = new PdfTaskResourcesInfo();
newResource.assign(resource);
newResource.x -= pointInfo.offsetX;
newResource.y -= pointInfo.offsetY;
this._addResourceToPage(pointInfo.pageVerIndex, pointInfo.pageHorIndex, newResource);
}
private _paginateTimeMarker(timeMarker: PdfTimeMarkerInfo): void {
const hPagination = this._getTimeMarkerPagination(timeMarker);
const vPagination = this._getTimeMarkerPagination(timeMarker, true);
for(let i = 0; i < vPagination.length; i++)
for(let j = 0; j < hPagination.length; j++) {
const newMarker = new PdfTimeMarkerInfo();
newMarker.assign(timeMarker);
newMarker.start.x -= hPagination[j].offset;
newMarker.start.y -= vPagination[i].offset;
this._addTimeMarkerToPage(vPagination[i].pageIndex, hPagination[j].pageIndex, newMarker);
}
}
private _getTableStart(table: IGanttTableExportInfo): PageNavigation {
const start = new PageNavigation(this.pageBorders, 0, 0, 0, 0, this.correctedPageBottoms);
start.offset(table.position.x, table.position.y);
return start;
}
private _getPage(rowIndex: number, colIndex: number, forceCreate?: boolean): IGanttPageExportInfo {
if(forceCreate)
this._extendPageMatrixIfRequired(rowIndex, colIndex);
return this._pages[rowIndex] && this._pages[rowIndex][colIndex];
}
private _getTableOrCreate(page: IGanttPageExportInfo, tableKey: string): IGanttTableExportInfo {
page.tables[tableKey] ??= this._createTable(tableKey);
return page.tables[tableKey];
}
private _preparePagesNavigationMatrixForTable(start: PageNavigation, table: IGanttTableExportInfo): Array<Array<CellNavigationInfo>> {
const matrix = new Array<Array<CellNavigationInfo>>();
const verticalVector = this._getTableNavigationVector(start, table, true);
const rowCount = verticalVector.length;
for(let i = 0; i < rowCount; i++) {
const row = new Array<CellNavigationInfo>();
const vInfo = verticalVector[i];
const horizontalVector = this._getTableNavigationVector(start, table, false, vInfo.globalCellIndex);
const colCount = horizontalVector.length;
for(let j = 0; j < colCount; j++) {
const hInfo = horizontalVector[j];
const cancelTextCut = table.name === PdfPageTableNames.chartScaleTop;
const cell = this._prepareCuttedCell(table.cells[vInfo.globalCellIndex][hInfo.globalCellIndex], hInfo, vInfo, cancelTextCut);
const info = new CellNavigationInfo(hInfo.pageIndex, vInfo.pageIndex, vInfo.cellIndexOnPage, hInfo.cellIndexOnPage, cell);
row.push(info);
}
matrix.push(row);
}
return matrix;
}
private _setTablePositionOnPage(page: IGanttPageExportInfo, tableKey: string, x: number, y: number): void {
const table = this._getTableOrCreate(page, tableKey);
table.position = new Point(x, y);
}
private _extendPageMatrixIfRequired(rowIndex: number, colIndex: number): void {
this._pages ??= new Array<Array<IGanttPageExportInfo>>();
for(let i = this._pages.length; i <= rowIndex; i++)
this._pages.push(new Array<IGanttPageExportInfo>());
const row = this._pages[rowIndex];
for(let i = row.length; i <= colIndex; i++)
row.push(this._createPage());
}
private _getTableAndExtendIfRequired(page: IGanttPageExportInfo, tableKey: string, rowIndex: number, colIndex: number): IGanttTableExportInfo {
const table = this._getTableOrCreate(page, tableKey);
const cells = table.cells;
for(let i = cells.length; i <= rowIndex; i++)
cells.push(new Array<CellDef>());
const row = cells[rowIndex];
for(let i = row.length; i <= colIndex; i++)
row.push(new CellDef());
return table;
}
private _createPage(): IGanttPageExportInfo {
return {
objects: {
tasks: null,
dependencies: null,
resources: null,
timeMarkers: null
},
tables: { }
};
}
private _createTable(tableKey: string): IGanttTableExportInfo {
const globalTableInfo = this._globalInfo?.tables[tableKey] as IGanttTableExportInfo;
return {
name: tableKey,
size: null,
position: null,
style: globalTableInfo.style,
baseCellSize: globalTableInfo.baseCellSize,
cells: new Array<Array<CellDef>>(),
hideRowLines: globalTableInfo.hideRowLines
};
}
private _addCellToPage(page: IGanttPageExportInfo, tableKey: string, cellInfo: CellNavigationInfo): void {
const rowIndex = cellInfo.cellRowIndexOnPage;
const colIndex = cellInfo.cellColIndexOnPage;
const table = this._getTableAndExtendIfRequired(page, tableKey, rowIndex, colIndex);
table.cells[rowIndex][colIndex].assign(cellInfo.cell);
}
private _updateTableSizeOnPages(tableKey: string):void {
const colCount = this._pages[0]?.length;
const rowCount = this._pages.length;
for(let i = 0; i < rowCount; i++)
for(let j = 0; j < colCount; j++)
this._updateTableSizeOnPage(this._pages[i][j], tableKey);
}
private _updateTableSizeOnPage(page: IGanttPageExportInfo, tableKey: string): void {
const table = page?.tables[tableKey] as IGanttTableExportInfo;
if(table) {
const rowCount = table.cells.length;
const height = rowCount * table.baseCellSize.height || 0;
const width = table.cells[0]?.reduce((acc, v, i) => acc += this._getCellWidth(table, 0, i), 0) || 0;
table.size = new Size(width, height);
}
}
private _getTableNavigationVector(start: PageNavigation, table: IGanttTableExportInfo, isVertical: boolean = false, rowIndex: number = 0): Array<VectorInfo> {
const vector = new Array<VectorInfo>();
const pageNav = PageNavigation.createFrom(start);
const length = isVertical ? table.cells?.length : table.cells[rowIndex]?.length;
for(let i = 0; i < length; i++) {
const cellSize = isVertical ? table.baseCellSize.height : this._getCellWidth(table, rowIndex, i);
this._placeCell(vector, pageNav, i, cellSize, isVertical);
}
return vector;
}
private _placeCell(vector: Array<VectorInfo>, pageNav: PageNavigation, cellGlobalIndex: number, size: number, isVertical: boolean): void {
const startPageIndex = isVertical ? pageNav.vIndex : pageNav.hIndex;
let pageOffsetIndex = vector[vector.length - 1]?.pageOffset ?? cellGlobalIndex;
let unplacedSize = size;
let spaceToBorder = pageNav.getSpaceToBorder(isVertical);
pageNav.offsetOneD(size, isVertical);
const endPageIndex = isVertical ? pageNav.vIndex : pageNav.hIndex;
if(!isVertical)
for(let i = startPageIndex; i < endPageIndex; i++) {
const info = new VectorInfo(i, cellGlobalIndex, pageOffsetIndex, spaceToBorder);
pageOffsetIndex = cellGlobalIndex;
vector.push(info);
unplacedSize -= spaceToBorder;
spaceToBorder = pageNav.getPageSize(isVertical);
}
if(endPageIndex !== startPageIndex)
pageOffsetIndex = cellGlobalIndex;
const isCutted = unplacedSize !== size;
const info = new VectorInfo(endPageIndex, cellGlobalIndex, pageOffsetIndex, isCutted ? unplacedSize : null);
vector.push(info);
}
private _prepareCuttedCell(originCell: CellDef, hInfo: VectorInfo, vInfo: VectorInfo, cancelTextCut?: boolean): CellDef {
const cell = new CellDef(originCell);
if(hInfo.isCutted) {
const width = hInfo.cutSize;
if(!cancelTextCut) {
const text = cell.content;
const style = originCell.styles;
const leftPadding = style && style.cellPadding.left || 0;
const rightPadding = style && style.cellPadding.right || 0;
const textWidth = width - leftPadding - rightPadding;
const parts = this._pdfDoc.splitTextToSize(text, textWidth);
originCell.content = text.replace(parts[0], "");
cell.content = parts[0];
}
cell.styles.cellWidth.assign(width);
}
if(vInfo.isCutted)
cell.styles.minCellHeight = vInfo.cutSize;
return cell;
}
private _getCellWidth(table: IGanttTableExportInfo, rowIndex: number, colIndex: number) {
const cell = table.cells[rowIndex][colIndex];
const style = cell.styles;
const numWidth = style.cellWidth.getValue() as number;
const width = numWidth ?? style.minCellWidth;
return width ?? table.baseCellSize.width * (cell.colSpan ?? 1);
}
private _getTaskPagination(task: PdfTaskInfo, isVertical?: boolean): Array<Record<string, any>> {
const size = isVertical ? task.height : task.width;
const startPos = isVertical ? task.top : task.left;
return this._getLinePagination(startPos, size, isVertical);
}
private _getDependencyLinePagination(dependency: PdfDependencyLineInfo, isVertical?: boolean): Array<Record<string, any>> {
const lineStart = dependency.points[0];
const lineEnd = dependency.points[1];
const size = isVertical ? lineEnd.y - lineStart.y : lineEnd.x - lineStart.x;
const startPos = isVertical ? lineStart.y : lineStart.x;
return this._getLinePagination(startPos, size, isVertical);
}
private _getTimeMarkerPagination(timeMarker: PdfTimeMarkerInfo, isVertical?: boolean): Array<Record<string, any>> {
const size = isVertical ? timeMarker.size.height : timeMarker.size.width;
const start = isVertical ? timeMarker.start.y : timeMarker.start.x;
return this._getLinePagination(start, size, isVertical);
}
private _getLinePagination(globalStart: number, size: number, isVertical?: boolean): Array<Record<string, any>> {
const result = new Array<Record<string, any>>();
const pageNav = this.pageNavigator.clone();
pageNav.offsetOneD(globalStart, isVertical);
const startPageIndex = isVertical ? pageNav.vIndex : pageNav.hIndex;
pageNav.offsetOneD(size, isVertical);
const endPageIndex = isVertical ? pageNav.vIndex : pageNav.hIndex;
for(let i = startPageIndex; i <= endPageIndex; i++)
result.push({
offset: pageNav.getPageGlobalOffset(i, isVertical),
pageIndex: i
});
return result;
}
private _getPointPageInfo(p: Point): Record<string, any> {
const pageNav = this.pageNavigator.clone();
pageNav.offset(p.x, p.y);
return {
offsetX: pageNav.getPageGlobalOffset(pageNav.hIndex),
offsetY: pageNav.getPageGlobalOffset(pageNav.vIndex, true),
pageHorIndex: pageNav.hIndex,
pageVerIndex: pageNav.vIndex
};
}
protected get pageWidth(): number {
return this._pdfDoc?.getPageWidth();
}
protected get pageHeight(): number {
return this._pdfDoc?.getPageHeight();
}
protected get pageLeftMargin(): number {
return this._props?.margins.left;
}
protected get pageTopMargin(): number {
return this._props?.margins.top;
}
protected get pageRightMargin(): number {
return this._props?.margins.right;
}
protected get pageBottomMargin(): number {
return this._props?.margins.bottom;
}
protected get pageLeft(): number {
this._pageLeft ??= this.pageLeftMargin;
return this._pageLeft;
}
protected get pageRight(): number {
this._pageRight ??= this.pageWidth - this.pageRightMargin;
return this._pageRight;
}
protected get pageTop(): number {
this._pageTop ??= this.pageTopMargin;
return this._pageTop;
}
protected get pageBottom(): number {
this._pageBottom ??= this.pageHeight - this.pageBottomMargin;
return this._pageBottom;
}
protected get pageBorders(): Record<string, number> {
return {
left: this.pageLeft,
top: this.pageTop,
bottom: this.pageBottom,
right: this.pageRight
};
}
protected get correctedPageBottoms(): Array<number> {
this._correctedPageBottoms ??= this._getCorrectedPagesBottom();
return this._correctedPageBottoms;
}
protected get pageNavigator(): PageNavigation {
this._pageNavigator ??= new PageNavigation(this.pageBorders, 0, 0, 0, 0, this.correctedPageBottoms);
return this._pageNavigator;
}
public _getCorrectedPagesBottom(): Array<number> {
const result = new Array<number>();
const tables = this._globalInfo?.tables;
const referenceTable = tables[PdfPageTableNames.treeListMain] ?? tables[PdfPageTableNames.chartMain];
const nav = new PageNavigation(this.pageBorders);
nav.pageY = referenceTable.position.y;
for(let i = 0; i < referenceTable.cells.length; i++) {
const cell = referenceTable.cells[i][0];
const height = cell.styles?.minCellHeight ?? referenceTable.baseCellSize.height;
const prevPageIndex = nav.vIndex;
const prevPageY = nav.pageY;
nav.offsetOneD(height, true);
if(prevPageIndex !== nav.vIndex) {
result.push(prevPageY);
nav.pageY = nav.getPageStart(true) + height;
}
}
return result;
}
private _addTaskToPage(pageVIndex: number, pageHIndex: number, task: PdfTaskInfo): void {
const page = this._getPage(pageVIndex, pageHIndex);
if(page) {
page.objects.tasks ??= new Array<PdfTaskInfo>();
page.objects.tasks.push(task);
}
}
private _addDependencyToPage(pageVIndex: number, pageHIndex: number, dependency: PdfDependencyLineInfo): void {
const page = this._getPage(pageVIndex, pageHIndex);
if(page) {
page.objects.dependencies ??= new Array<PdfDependencyLineInfo>();
page.objects.dependencies.push(dependency);
}
}
private _addResourceToPage(pageVIndex: number, pageHIndex: number, resource: PdfTaskResourcesInfo): void {
const page = this._getPage(pageVIndex, pageHIndex);
if(page) {
page.objects.resources ??= new Array<PdfTaskResourcesInfo>();
page.objects.resources.push(resource);
}
}
private _addTimeMarkerToPage(pageVIndex: number, pageHIndex: number, timeMarker: PdfTimeMarkerInfo): void {
const page = this._getPage(pageVIndex, pageHIndex);
if(page) {
page.objects.timeMarkers ??= new Array<PdfTimeMarkerInfo>();
page.objects.timeMarkers.push(timeMarker);
}
}
private _offsetPoints(points: Array<Point>, offsetX: number, offsetY: number): void {
points.forEach(p => {
p.x -= offsetX;
p.y -= offsetY;
});
}
}