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