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