UNPKG

devexpress-gantt

Version:

DevExpress Gantt Control

483 lines (474 loc) 22.9 kB
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; }); } }