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