001 /* 002 // $Id:$ 003 // This software is subject to the terms of the Eclipse Public License v1.0 004 // Agreement, available at the following URL: 005 // http://www.eclipse.org/legal/epl-v10.html. 006 // Copyright (C) 2009-2009 Julian Hyde 007 // All Rights Reserved. 008 // You must accept the terms of that agreement to use this software. 009 */ 010 package org.olap4j.layout; 011 012 import org.olap4j.*; 013 import org.olap4j.metadata.Member; 014 import org.olap4j.impl.CoordinateIterator; 015 import org.olap4j.impl.Olap4jUtil; 016 017 import java.io.PrintWriter; 018 import java.util.*; 019 020 /** 021 * Formatter that can convert a {@link CellSet} into a two-dimensional text 022 * layout. 023 * 024 * <p>With non-compact layout: 025 * 026 * <pre> 027 * | 1997 | 028 * | Q1 | Q2 | 029 * | | 4 | 030 * | Unit Sales | Store Sales | Unit Sales | Store Sales | 031 * ----+----+---------+------------+-------------+------------+-------------+ 032 * USA | CA | Modesto | 12 | 34.5 | 13 | 35.60 | 033 * | WA | Seattle | 12 | 34.5 | 13 | 35.60 | 034 * | CA | Fresno | 12 | 34.5 | 13 | 35.60 | 035 * </pre> 036 * 037 * <p>With compact layout: 038 * <pre> 039 * 040 * 1997 041 * Q1 Q2 042 * 4 043 * Unit Sales Store Sales Unit Sales Store Sales 044 * === == ======= ========== =========== ========== =========== 045 * USA CA Modesto 12 34.5 13 35.60 046 * WA Seattle 12 34.5 13 35.60 047 * CA Fresno 12 34.5 13 35.60 048 * </pre> 049 * 050 * <p><b>This class is experimental. It is not part of the olap4j 051 * specification and is subject to change without notice.</b></p> 052 * 053 * @author jhyde 054 * @version $Id:$ 055 * @since Apr 15, 2009 056 */ 057 public class RectangularCellSetFormatter implements CellSetFormatter { 058 private final boolean compact; 059 060 /** 061 * Creates a RectangularCellSetFormatter. 062 * 063 * @param compact Whether to generate compact output 064 */ 065 public RectangularCellSetFormatter(boolean compact) { 066 this.compact = compact; 067 } 068 069 public void format(CellSet cellSet, PrintWriter pw) { 070 // Compute how many rows are required to display the columns axis. 071 // In the example, this is 4 (1997, Q1, space, Unit Sales) 072 final CellSetAxis columnsAxis; 073 if (cellSet.getAxes().size() > 0) { 074 columnsAxis = cellSet.getAxes().get(0); 075 } else { 076 columnsAxis = null; 077 } 078 AxisInfo columnsAxisInfo = computeAxisInfo(columnsAxis); 079 080 // Compute how many columns are required to display the rows axis. 081 // In the example, this is 3 (the width of USA, CA, Los Angeles) 082 final CellSetAxis rowsAxis; 083 if (cellSet.getAxes().size() > 1) { 084 rowsAxis = cellSet.getAxes().get(1); 085 } else { 086 rowsAxis = null; 087 } 088 AxisInfo rowsAxisInfo = computeAxisInfo(rowsAxis); 089 090 if (cellSet.getAxes().size() > 2) { 091 int[] dimensions = new int[cellSet.getAxes().size() - 2]; 092 for (int i = 2; i < cellSet.getAxes().size(); i++) { 093 CellSetAxis cellSetAxis = cellSet.getAxes().get(i); 094 dimensions[i - 2] = cellSetAxis.getPositions().size(); 095 } 096 for (int[] pageCoords : CoordinateIterator.iterate(dimensions)) { 097 formatPage( 098 cellSet, 099 pw, 100 pageCoords, 101 columnsAxis, 102 columnsAxisInfo, 103 rowsAxis, 104 rowsAxisInfo); 105 } 106 } else { 107 formatPage( 108 cellSet, 109 pw, 110 new int[] {}, 111 columnsAxis, 112 columnsAxisInfo, 113 rowsAxis, 114 rowsAxisInfo); 115 } 116 } 117 118 /** 119 * Formats a two-dimensional page. 120 * 121 * @param cellSet Cell set 122 * @param pw Print writer 123 * @param pageCoords Coordinates of page [page, chapter, section, ...] 124 * @param columnsAxis Columns axis 125 * @param columnsAxisInfo Description of columns axis 126 * @param rowsAxis Rows axis 127 * @param rowsAxisInfo Description of rows axis 128 */ 129 private void formatPage( 130 CellSet cellSet, 131 PrintWriter pw, 132 int[] pageCoords, 133 CellSetAxis columnsAxis, 134 AxisInfo columnsAxisInfo, 135 CellSetAxis rowsAxis, 136 AxisInfo rowsAxisInfo) 137 { 138 if (pageCoords.length > 0) { 139 pw.println(); 140 for (int i = pageCoords.length - 1; i >= 0; --i) { 141 int pageCoord = pageCoords[i]; 142 final CellSetAxis axis = cellSet.getAxes().get(2 + i); 143 pw.print(axis.getAxisOrdinal() + ": "); 144 final Position position = 145 axis.getPositions().get(pageCoord); 146 int k = -1; 147 for (Member member : position.getMembers()) { 148 if (++k > 0) { 149 pw.print(", "); 150 } 151 pw.print(member.getUniqueName()); 152 } 153 pw.println(); 154 } 155 } 156 // Figure out the dimensions of the blank rectangle in the top left 157 // corner. 158 final int yOffset = columnsAxisInfo.getWidth(); 159 final int xOffsset = rowsAxisInfo.getWidth(); 160 161 // Populate a string matrix 162 Matrix matrix = 163 new Matrix( 164 xOffsset 165 + (columnsAxis == null 166 ? 1 167 : columnsAxis.getPositions().size()), 168 yOffset 169 + (rowsAxis == null 170 ? 1 171 : rowsAxis.getPositions().size())); 172 173 // Populate corner 174 for (int x = 0; x < xOffsset; x++) { 175 for (int y = 0; y < yOffset; y++) { 176 matrix.set(x, y, "", false, x > 0); 177 } 178 } 179 180 // Populate matrix with cells representing axes 181 //noinspection SuspiciousNameCombination 182 populateAxis( 183 matrix, columnsAxis, columnsAxisInfo, true, xOffsset); 184 populateAxis( 185 matrix, rowsAxis, rowsAxisInfo, false, yOffset); 186 187 // Populate cell values 188 for (Cell cell : cellIter(pageCoords, cellSet)) { 189 final List<Integer> coordList = cell.getCoordinateList(); 190 int x = xOffsset; 191 if (coordList.size() > 0) { 192 x += coordList.get(0); 193 } 194 int y = yOffset; 195 if (coordList.size() > 1) { 196 y += coordList.get(1); 197 } 198 matrix.set( 199 x, y, cell.getFormattedValue(), true, false); 200 } 201 202 int[] columnWidths = new int[matrix.width]; 203 int widestWidth = 0; 204 for (int x = 0; x < matrix.width; x++) { 205 int columnWidth = 0; 206 for (int y = 0; y < matrix.height; y++) { 207 MatrixCell cell = matrix.get(x, y); 208 if (cell != null) { 209 columnWidth = 210 Math.max(columnWidth, cell.value.length()); 211 } 212 } 213 columnWidths[x] = columnWidth; 214 widestWidth = Math.max(columnWidth, widestWidth); 215 } 216 217 // Create a large array of spaces, for efficient printing. 218 char[] spaces = new char[widestWidth + 1]; 219 Arrays.fill(spaces, ' '); 220 char[] equals = new char[widestWidth + 1]; 221 Arrays.fill(equals, '='); 222 char[] dashes = new char[widestWidth + 3]; 223 Arrays.fill(dashes, '-'); 224 225 if (compact) { 226 for (int y = 0; y < matrix.height; y++) { 227 for (int x = 0; x < matrix.width; x++) { 228 if (x > 0) { 229 pw.print(' '); 230 } 231 final MatrixCell cell = matrix.get(x, y); 232 final int len; 233 if (cell != null) { 234 if (cell.sameAsPrev) { 235 len = 0; 236 } else { 237 if (cell.right) { 238 int padding = 239 columnWidths[x] - cell.value.length(); 240 pw.write(spaces, 0, padding); 241 pw.print(cell.value); 242 continue; 243 } 244 pw.print(cell.value); 245 len = cell.value.length(); 246 } 247 } else { 248 len = 0; 249 } 250 if (x == matrix.width - 1) { 251 // at last column; don't bother to print padding 252 break; 253 } 254 int padding = columnWidths[x] - len; 255 pw.write(spaces, 0, padding); 256 } 257 pw.println(); 258 if (y == yOffset - 1) { 259 for (int x = 0; x < matrix.width; x++) { 260 if (x > 0) { 261 pw.write(' '); 262 } 263 pw.write(equals, 0, columnWidths[x]); 264 } 265 pw.println(); 266 } 267 } 268 } else { 269 for (int y = 0; y < matrix.height; y++) { 270 for (int x = 0; x < matrix.width; x++) { 271 final MatrixCell cell = matrix.get(x, y); 272 final int len; 273 if (cell != null) { 274 if (cell.sameAsPrev) { 275 pw.print(" "); 276 len = 0; 277 } else { 278 pw.print("| "); 279 if (cell.right) { 280 int padding = 281 columnWidths[x] - cell.value.length(); 282 pw.write(spaces, 0, padding); 283 pw.print(cell.value); 284 pw.print(' '); 285 continue; 286 } 287 pw.print(cell.value); 288 len = cell.value.length(); 289 } 290 } else { 291 pw.print("| "); 292 len = 0; 293 } 294 int padding = columnWidths[x] - len; 295 ++padding; 296 pw.write(spaces, 0, padding); 297 } 298 pw.println('|'); 299 if (y == yOffset - 1) { 300 for (int x = 0; x < matrix.width; x++) { 301 pw.write('+'); 302 pw.write(dashes, 0, columnWidths[x] + 2); 303 } 304 pw.println('+'); 305 } 306 } 307 } 308 } 309 310 /** 311 * Populates cells in the matrix corresponding to a particular axis. 312 * 313 * @param matrix Matrix to populate 314 * @param axis Axis 315 * @param axisInfo Description of axis 316 * @param isColumns True if columns, false if rows 317 * @param offset Ordinal of first cell to populate in matrix 318 */ 319 private void populateAxis( 320 Matrix matrix, 321 CellSetAxis axis, 322 AxisInfo axisInfo, 323 boolean isColumns, 324 int offset) 325 { 326 if (axis == null) { 327 return; 328 } 329 Member[] prevMembers = new Member[axisInfo.getWidth()]; 330 Member[] members = new Member[axisInfo.getWidth()]; 331 for (int i = 0; i < axis.getPositions().size(); i++) { 332 final int x = offset + i; 333 Position position = axis.getPositions().get(i); 334 int yOffset = 0; 335 final List<Member> memberList = position.getMembers(); 336 for (int j = 0; j < memberList.size(); j++) { 337 Member member = memberList.get(j); 338 final AxisOrdinalInfo ordinalInfo = 339 axisInfo.ordinalInfos.get(j); 340 while (member != null) { 341 if (member.getDepth() < ordinalInfo.minDepth) { 342 break; 343 } 344 final int y = 345 yOffset 346 + member.getDepth() 347 - ordinalInfo.minDepth; 348 members[y] = member; 349 member = member.getParentMember(); 350 } 351 yOffset += ordinalInfo.getWidth(); 352 } 353 boolean same = true; 354 for (int y = 0; y < members.length; y++) { 355 Member member = members[y]; 356 same = 357 same 358 && i > 0 359 && Olap4jUtil.equal(prevMembers[y], member); 360 String value = 361 member == null 362 ? "" 363 : member.getCaption(null); 364 if (isColumns) { 365 matrix.set(x, y, value, false, same); 366 } else { 367 if (same) { 368 value = ""; 369 } 370 //noinspection SuspiciousNameCombination 371 matrix.set(y, x, value, false, false); 372 } 373 prevMembers[y] = member; 374 members[y] = null; 375 } 376 } 377 } 378 379 /** 380 * Computes a description of an axis. 381 * 382 * @param axis Axis 383 * @return Description of axis 384 */ 385 private AxisInfo computeAxisInfo(CellSetAxis axis) 386 { 387 if (axis == null) { 388 return new AxisInfo(0); 389 } 390 final AxisInfo axisInfo = 391 new AxisInfo(axis.getAxisMetaData().getHierarchies().size()); 392 int p = -1; 393 for (Position position : axis.getPositions()) { 394 ++p; 395 int k = -1; 396 for (Member member : position.getMembers()) { 397 ++k; 398 final AxisOrdinalInfo axisOrdinalInfo = 399 axisInfo.ordinalInfos.get(k); 400 final int topDepth = 401 member.isAll() 402 ? member.getDepth() 403 : member.getHierarchy().hasAll() 404 ? 1 405 : 0; 406 if (axisOrdinalInfo.minDepth > topDepth 407 || p == 0) 408 { 409 axisOrdinalInfo.minDepth = topDepth; 410 } 411 axisOrdinalInfo.maxDepth = 412 Math.max( 413 axisOrdinalInfo.maxDepth, 414 member.getDepth()); 415 } 416 } 417 return axisInfo; 418 } 419 420 /** 421 * Returns an iterator over cells in a result. 422 */ 423 private static Iterable<Cell> cellIter( 424 final int[] pageCoords, 425 final CellSet cellSet) 426 { 427 return new Iterable<Cell>() { 428 public Iterator<Cell> iterator() { 429 int[] axisDimensions = 430 new int[cellSet.getAxes().size() - pageCoords.length]; 431 assert pageCoords.length <= axisDimensions.length; 432 for (int i = 0; i < axisDimensions.length; i++) { 433 CellSetAxis axis = cellSet.getAxes().get(i); 434 axisDimensions[i] = axis.getPositions().size(); 435 } 436 final CoordinateIterator coordIter = 437 new CoordinateIterator(axisDimensions, true); 438 return new Iterator<Cell>() { 439 public boolean hasNext() { 440 return coordIter.hasNext(); 441 } 442 443 public Cell next() { 444 final int[] ints = coordIter.next(); 445 final AbstractList<Integer> intList = 446 new AbstractList<Integer>() { 447 public Integer get(int index) { 448 return index < ints.length 449 ? ints[index] 450 : pageCoords[index - ints.length]; 451 } 452 453 public int size() { 454 return pageCoords.length + ints.length; 455 } 456 }; 457 return cellSet.getCell(intList); 458 } 459 460 public void remove() { 461 throw new UnsupportedOperationException(); 462 } 463 }; 464 } 465 }; 466 } 467 468 /** 469 * Description of a particular hierarchy mapped to an axis. 470 */ 471 private static class AxisOrdinalInfo { 472 int minDepth = 1; 473 int maxDepth = 0; 474 475 /** 476 * Returns the number of matrix columns required to display this 477 * hierarchy. 478 */ 479 public int getWidth() { 480 return maxDepth - minDepth + 1; 481 } 482 } 483 484 /** 485 * Description of an axis. 486 */ 487 private static class AxisInfo { 488 final List<AxisOrdinalInfo> ordinalInfos; 489 490 /** 491 * Creates an AxisInfo. 492 * 493 * @param ordinalCount Number of hierarchies on this axis 494 */ 495 AxisInfo(int ordinalCount) { 496 ordinalInfos = new ArrayList<AxisOrdinalInfo>(ordinalCount); 497 for (int i = 0; i < ordinalCount; i++) { 498 ordinalInfos.add(new AxisOrdinalInfo()); 499 } 500 } 501 502 /** 503 * Returns the number of matrix columns required by this axis. The 504 * sum of the width of the hierarchies on this axis. 505 * 506 * @return Width of axis 507 */ 508 public int getWidth() { 509 int width = 0; 510 for (AxisOrdinalInfo info : ordinalInfos) { 511 width += info.getWidth(); 512 } 513 return width; 514 } 515 } 516 517 /** 518 * Two-dimensional collection of string values. 519 */ 520 private class Matrix { 521 private final Map<List<Integer>, MatrixCell> map = 522 new HashMap<List<Integer>, MatrixCell>(); 523 private final int width; 524 private final int height; 525 526 /** 527 * Creats a Matrix. 528 * 529 * @param width Width of matrix 530 * @param height Height of matrix 531 */ 532 public Matrix(int width, int height) { 533 this.width = width; 534 this.height = height; 535 } 536 537 /** 538 * Sets the value at a particular coordinate 539 * 540 * @param x X coordinate 541 * @param y Y coordinate 542 * @param value Value 543 */ 544 void set(int x, int y, String value) { 545 set(x, y, value, false, false); 546 } 547 548 /** 549 * Sets the value at a particular coordinate 550 * 551 * @param x X coordinate 552 * @param y Y coordinate 553 * @param value Value 554 * @param right Whether value is right-justified 555 * @param sameAsPrev Whether value is the same as the previous value. 556 * If true, some formats separators between cells 557 */ 558 void set( 559 int x, 560 int y, 561 String value, 562 boolean right, 563 boolean sameAsPrev) 564 { 565 map.put( 566 Arrays.asList(x, y), 567 new MatrixCell(value, right, sameAsPrev)); 568 assert x >= 0 && x < width : x; 569 assert y >= 0 && y < height : y; 570 } 571 572 /** 573 * Returns the cell at a particular coordinate. 574 * 575 * @param x X coordinate 576 * @param y Y coordinate 577 * @return Cell 578 */ 579 public MatrixCell get(int x, int y) { 580 return map.get(Arrays.asList(x, y)); 581 } 582 } 583 584 /** 585 * Contents of a cell in a matrix. 586 */ 587 private static class MatrixCell { 588 final String value; 589 final boolean right; 590 final boolean sameAsPrev; 591 592 /** 593 * Creates a matrix cell. 594 * 595 * @param value Value 596 * @param right Whether value is right-justified 597 * @param sameAsPrev Whether value is the same as the previous value. 598 * If true, some formats separators between cells 599 */ 600 MatrixCell( 601 String value, 602 boolean right, 603 boolean sameAsPrev) 604 { 605 this.value = value; 606 this.right = right; 607 this.sameAsPrev = sameAsPrev; 608 } 609 } 610 } 611 612 // End RectangularCellSetFormatter.java