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*/
020package org.olap4j.layout;
021
022import org.olap4j.*;
023import org.olap4j.impl.CoordinateIterator;
024import org.olap4j.impl.Olap4jUtil;
025import org.olap4j.metadata.Member;
026
027import java.io.PrintWriter;
028import 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*/
067public 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