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