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