OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 93 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.openoffice.spreadsheet;
15
 
80 ilm 16
import org.openconcerto.openoffice.Length;
21 ilm 17
import org.openconcerto.openoffice.LengthUnit;
17 ilm 18
import org.openconcerto.openoffice.ODDocument;
20 ilm 19
import org.openconcerto.openoffice.Style;
25 ilm 20
import org.openconcerto.openoffice.StyleDesc;
20 ilm 21
import org.openconcerto.openoffice.StyleStyleDesc;
73 ilm 22
import org.openconcerto.openoffice.StyledNode;
17 ilm 23
import org.openconcerto.openoffice.XMLVersion;
73 ilm 24
import org.openconcerto.openoffice.spreadsheet.CellStyle.StyleTableCellProperties;
17 ilm 25
import org.openconcerto.openoffice.spreadsheet.SheetTableModel.MutableTableModel;
26
import org.openconcerto.utils.CollectionUtils;
27
import org.openconcerto.utils.Tuple2;
28
import org.openconcerto.xml.JDOMUtils;
21 ilm 29
import org.openconcerto.xml.SimpleXMLPath;
17 ilm 30
 
31
import java.awt.Point;
32
import java.util.ArrayList;
21 ilm 33
import java.util.HashMap;
17 ilm 34
import java.util.HashSet;
35
import java.util.List;
21 ilm 36
import java.util.Map;
17 ilm 37
import java.util.Set;
38
import java.util.regex.Matcher;
39
 
40
import javax.swing.table.TableModel;
41
 
42
import org.jdom.Attribute;
43
import org.jdom.Element;
44
 
45
/**
46
 * A single sheet in a spreadsheet.
47
 *
48
 * @author Sylvain
49
 * @param <D> type of table parent
50
 */
51
public class Table<D extends ODDocument> extends TableCalcNode<TableStyle, D> {
52
 
53
    static Element createEmpty(XMLVersion ns) {
65 ilm 54
        return createEmpty(ns, 1, 1);
55
    }
56
 
57
    static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount) {
73 ilm 58
        return createEmpty(ns, colCount, rowCount, null);
59
    }
60
 
61
    // pass styleName to avoid creating possibly expensive Table instance (readCols/readRows) in
62
    // order to use setStyleName()
63
    static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount, final String styleName) {
65 ilm 64
        // from the relaxNG
65
        if (colCount < 1 || rowCount < 1)
66
            throw new IllegalArgumentException("a table must have at least one cell");
17 ilm 67
        final Element col = Column.createEmpty(ns, null);
65 ilm 68
        Axis.COLUMN.setRepeated(col, colCount);
69
        final Element row = Row.createEmpty(ns).addContent(Cell.createEmpty(ns, colCount));
70
        Axis.ROW.setRepeated(row, rowCount);
73 ilm 71
        final Element res = new Element("table", ns.getTABLE()).addContent(col).addContent(row);
72
        StyledNode.setStyleName(res, styleName);
73
        return res;
17 ilm 74
    }
75
 
76
    static final String getName(final Element elem) {
77
        return elem.getAttributeValue("name", elem.getNamespace("table"));
78
    }
79
 
80
    // ATTN Row have their index as attribute
25 ilm 81
    private final ArrayList<Row<D>> rows;
82
    private TableGroup rowGroup;
83
    private final ArrayList<Column<D>> cols;
84
    private TableGroup columnGroup;
17 ilm 85
 
86
    public Table(D parent, Element local) {
87
        super(parent, local, TableStyle.class);
88
 
25 ilm 89
        this.rows = new ArrayList<Row<D>>(64);
90
        this.cols = new ArrayList<Column<D>>(32);
17 ilm 91
 
25 ilm 92
        // read columns first since Row constructor needs it
17 ilm 93
        this.readColumns();
94
        this.readRows();
95
    }
96
 
97
    private void readColumns() {
25 ilm 98
        this.read(Axis.COLUMN);
17 ilm 99
    }
100
 
101
    private final void readRows() {
25 ilm 102
        this.read(Axis.ROW);
17 ilm 103
    }
104
 
25 ilm 105
    private final void read(final Axis axis) {
106
        final boolean col = axis == Axis.COLUMN;
107
        final Tuple2<TableGroup, List<Element>> r = TableGroup.createRoot(this, axis);
108
        final ArrayList<?> l = col ? this.cols : this.rows;
109
        final int oldSize = l.size();
110
        l.clear();
111
        final int newSize = r.get0().getSize();
112
        l.ensureCapacity(newSize);
113
        if (col) {
114
            final StyleStyleDesc<ColumnStyle> colStyleDesc = getColumnStyleDesc();
115
            for (final Element clone : r.get1())
116
                this.addCol(clone, colStyleDesc);
117
            this.columnGroup = r.get0();
118
        } else {
119
            final StyleStyleDesc<RowStyle> rowStyleDesc = getRowStyleDesc();
120
            final StyleStyleDesc<CellStyle> cellStyleDesc = getCellStyleDesc();
121
            for (final Element clone : r.get1())
122
                this.addRow(clone, rowStyleDesc, cellStyleDesc);
123
            this.rowGroup = r.get0();
17 ilm 124
        }
25 ilm 125
        // this always copy the array, so make sure we reclaim enough memory (~ 64k)
126
        if (oldSize - newSize > 8192) {
127
            l.trimToSize();
128
        }
129
        assert newSize == (col ? this.getColumnCount() : this.getRowCount());
17 ilm 130
    }
131
 
25 ilm 132
    private final void addCol(Element clone, StyleStyleDesc<ColumnStyle> colStyleDesc) {
133
        this.cols.add(new Column<D>(this, clone, colStyleDesc));
17 ilm 134
    }
135
 
25 ilm 136
    static final int flattenChildren(final List<Element> res, final Element elem, final Axis axis) {
137
        int count = 0;
138
        // array so that flatten1() can modify an int
139
        int[] index = new int[] { 0 };
140
        // copy since we will change our children (don't use List.listIterator(int) since it
141
        // re-filters all content)
142
        @SuppressWarnings("unchecked")
143
        final List<Element> children = new ArrayList<Element>(elem.getChildren(axis.getElemName(), elem.getNamespace()));
144
        final int stop = children.size();
145
        for (int i = 0; i < stop; i++) {
146
            final Element row = children.get(i);
147
            count += flatten1(res, row, axis, index);
148
        }
149
        return count;
150
    }
17 ilm 151
 
25 ilm 152
    static int flatten1(final List<Element> res, final Element row, final Axis axis) {
153
        return flatten1(res, row, axis, null);
17 ilm 154
    }
155
 
25 ilm 156
    // add XML elements to res and return the logical count
157
    private static int flatten1(final List<Element> res, final Element row, final Axis axis, final int[] parentIndex) {
158
        final int resSize = res.size();
159
        final Attribute repeatedAttr = axis.getRepeatedAttr(row);
160
        final int repeated = repeatedAttr == null ? 1 : Integer.parseInt(repeatedAttr.getValue());
161
        if (axis == Axis.COLUMN && repeated > 1) {
162
            row.removeAttribute(repeatedAttr);
163
            final Element parent = row.getParentElement();
164
            final int index = (parentIndex == null ? parent.indexOf(row) : parentIndex[0]) + 1;
165
            res.add(row);
166
            // -1 : we keep the original row
167
            for (int i = 0; i < repeated - 1; i++) {
168
                final Element clone = (Element) row.clone();
169
                res.add(clone);
170
                parent.addContent(index + i, clone);
17 ilm 171
            }
25 ilm 172
        } else {
173
            res.add(row);
17 ilm 174
        }
25 ilm 175
        if (parentIndex != null)
176
            parentIndex[0] += res.size() - resSize;
177
        return repeated;
17 ilm 178
    }
179
 
180
    public final String getName() {
181
        return getName(this.getElement());
182
    }
183
 
184
    public final void setName(String name) {
185
        this.getElement().setAttribute("name", name, this.getODDocument().getVersion().getTABLE());
186
    }
187
 
188
    public void detach() {
189
        this.getElement().detach();
190
    }
191
 
192
    public final Object getPrintRanges() {
193
        return this.getElement().getAttributeValue("print-ranges", this.getTABLE());
194
    }
195
 
196
    public final void setPrintRanges(String s) {
197
        this.getElement().setAttribute("print-ranges", s, this.getTABLE());
198
    }
199
 
200
    public final void removePrintRanges() {
201
        this.getElement().removeAttribute("print-ranges", this.getTABLE());
202
    }
203
 
204
    public final synchronized void duplicateFirstRows(int nbFirstRows, int nbDuplicate) {
205
        this.duplicateRows(0, nbFirstRows, nbDuplicate);
206
    }
207
 
208
    public final synchronized void insertDuplicatedRows(int rowDuplicated, int nbDuplicate) {
209
        this.duplicateRows(rowDuplicated, 1, nbDuplicate);
210
    }
211
 
212
    /**
213
     * Clone a range of rows. Eg if you want to copy once rows 2 through 5, you call
214
     * <code>duplicateRows(2, 4, 1)</code>.
215
     *
216
     * @param start the first row to clone.
80 ilm 217
     * @param count the number of rows after <code>start</code> to clone, i.e. 1 means clone one
218
     *        row, must be greater or equal to 0.
219
     * @param copies the number of copies of the range to make, must be greater or equal to 0.
220
     * @throws IllegalArgumentException if arguments are not valid.
17 ilm 221
     */
80 ilm 222
    public final void duplicateRows(final int start, final int count, final int copies) throws IllegalArgumentException {
21 ilm 223
        this.duplicateRows(start, count, copies, true);
224
    }
19 ilm 225
 
80 ilm 226
    public final synchronized void duplicateRows(final int start, final int count, final int copies, final boolean updateCellAddresses) throws IllegalArgumentException {
227
        if (start < 0)
228
            throw new IllegalArgumentException("Negative start index : " + start);
229
        if (count < 0)
230
            throw new IllegalArgumentException("Negative count : " + count);
231
        else if (count == 0)
232
            return;
233
        if (copies < 0)
234
            throw new IllegalArgumentException("Negative copies : " + copies);
235
        else if (copies == 0)
236
            return;
237
        // stop is excluded
17 ilm 238
        final int stop = start + count;
80 ilm 239
        if (stop > this.getRowCount())
240
            throw new IllegalArgumentException("Last row to duplicate (" + (stop - 1) + ") doesn't exist, there's only : " + getRowCount());
241
 
21 ilm 242
        // should not change merged status
243
        final Map<Point, Integer> coverOrigins = new HashMap<Point, Integer>();
244
        final List<Point> coverOriginsToUpdate = new ArrayList<Point>();
245
        final int colCount = this.getColumnCount();
246
        for (int x = 0; x < colCount;) {
247
            int y = start;
248
            while (y < stop) {
249
                final Point coverOrigin = this.getCoverOrigin(x, y);
250
                if (coverOrigin == null) {
251
                    y++;
252
                } else {
253
                    final int lastCoveredCellRow;
254
                    // if we have already encountered this merged cell, skip it
255
                    if (coverOrigins.containsKey(coverOrigin)) {
256
                        lastCoveredCellRow = coverOrigins.get(coverOrigin);
257
                    } else {
258
                        final Cell<D> covering = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
259
                        lastCoveredCellRow = coverOrigin.y + covering.getRowsSpanned() - 1;
260
                        if (coverOrigin.y < start) {
261
                            if (lastCoveredCellRow < stop - 1)
262
                                throw new IllegalArgumentException("Span starts before the duplicated rows and doesn't extend past the end of duplicated rows at " + getAddress(coverOrigin));
263
                        } else {
264
                            if (lastCoveredCellRow > stop - 1)
265
                                throw new IllegalArgumentException("Span starts in the duplicated rows and extend past the end of duplicated rows at " + getAddress(coverOrigin));
266
                        }
267
 
268
                        coverOrigins.put(coverOrigin, lastCoveredCellRow);
269
                        // merged cells inside the duplicated rows don't need to be updated
270
                        if (coverOrigin.y < start || lastCoveredCellRow > stop - 1)
271
                            coverOriginsToUpdate.add(coverOrigin);
272
                    }
273
                    y = lastCoveredCellRow + 1;
274
                }
275
            }
276
            x++;
277
        }
278
 
17 ilm 279
        // clone xml elements and add them to our tree
280
        final List<Element> clones = new ArrayList<Element>(count * copies);
25 ilm 281
        for (int l = start; l < stop;) {
282
            final Row<D> immutableRow = this.getRow(l);
283
            final Row<D> toClone;
284
            // MAYBE use something else than getMutableRow() since we don't need a single row.
285
            // the repeated row starts before the copied range, split it at the beginning
286
            if (immutableRow.getY() < l) {
287
                toClone = this.getMutableRow(l);
288
            } else {
289
                assert immutableRow.getY() == l;
290
                if (immutableRow.getLastY() >= stop) {
291
                    // the repeated row goes beyond the copied range, split it at the end
292
                    assert this.getRow(stop) == immutableRow;
293
                    this.getMutableRow(stop);
294
                    toClone = this.getRow(l);
295
                } else {
296
                    toClone = immutableRow;
297
                }
17 ilm 298
            }
25 ilm 299
            assert toClone.getY() == l;
300
            assert toClone.getLastY() < stop : "Row goes to far";
301
            l += toClone.getRepeated();
302
            clones.add((Element) toClone.getElement().clone());
17 ilm 303
        }
25 ilm 304
        final int clonesSize = clones.size();
305
        for (int i = 1; i < copies; i++) {
306
            for (int j = 0; j < clonesSize; j++) {
307
                clones.add((Element) clones.get(j).clone());
308
            }
309
        }
17 ilm 310
        // works anywhere its XML element is
25 ilm 311
        assert this.getRow(stop - 1).getLastY() == stop - 1 : "Adding XML element too far";
312
        JDOMUtils.insertAfter(this.getRow(stop - 1).getElement(), clones);
17 ilm 313
 
21 ilm 314
        for (final Point coverOrigin : coverOriginsToUpdate) {
315
            final MutableCell<D> coveringCell = getCellAt(coverOrigin);
316
            coveringCell.setRowsSpanned(coveringCell.getRowsSpanned() + count * copies);
317
        }
318
 
25 ilm 319
        // synchronize our rows with our new tree (rows' index have changed)
17 ilm 320
        this.readRows();
19 ilm 321
 
21 ilm 322
        // 19.627 in OpenDocument-v1.2-cs01-part1 : The table:end-cell-address attribute specifies
323
        // end position of the shape if it is included in a spreadsheet document.
324
        if (updateCellAddresses && getODDocument() instanceof SpreadSheet) {
325
            final SpreadSheet ssheet = (SpreadSheet) getODDocument();
326
            final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.allAttributes("end-cell-address", "table");
327
            for (final Attribute endCellAttr : descAttrs.selectNodes(getElement())) {
328
                final Tuple2<Sheet, Point> resolved = ssheet.resolve(endCellAttr.getValue());
329
                final Sheet endCellSheet = resolved.get0();
330
                if (endCellSheet != this)
331
                    throw new UnsupportedOperationException("End sheet is not this : " + endCellSheet);
332
                final Point endCellPoint = resolved.get1();
333
                // if the end point is before the copied rows, nothing to do
334
                if (endCellPoint.y >= start) {
335
                    final Element endCellParentElem = endCellAttr.getParent();
19 ilm 336
 
21 ilm 337
                    // find row index of the shape
338
                    final Element rowElem = JDOMUtils.getAncestor(endCellParentElem, "table-row", getTABLE());
339
                    if (rowElem == null)
340
                        throw new IllegalStateException("Not in a row : " + JDOMUtils.output(endCellParentElem));
341
                    int startRowIndex = -1;
342
                    final int rowCount = getRowCount();
343
                    for (int i = 0; i < rowCount; i++) {
344
                        if (getRow(i).getElement() == rowElem) {
345
                            startRowIndex = i;
346
                            break;
347
                        }
348
                    }
349
                    if (startRowIndex < 0)
350
                        throw new IllegalStateException("Row not found for " + JDOMUtils.output(endCellParentElem));
351
                    final int newEndY;
352
                    if (startRowIndex >= start + (copies + 1) * count) {
353
                        // if the shape doesn't span over the copied rows, only need to offset
354
                        // end-cell-address
355
                        newEndY = endCellPoint.y + copies * count;
356
                    } else if (startRowIndex >= start + count && endCellPoint.y < start + count) {
357
                        // if the shape was copied and its end cell too, translate it
358
                        // find in which copy the shape is in, ATTN the truncation is important
359
                        // since the shape might not be in the first copied row
360
                        final int nth = (startRowIndex - start) / count;
361
                        newEndY = endCellPoint.y + nth * count;
362
                    } else {
363
                        // we must use height to compute new values for end-cell-address and end-y
19 ilm 364
 
21 ilm 365
                        // find the height of the shape
93 ilm 366
                        final Length[] coordinates = getODDocument().getFormatVersion().getXML().getCoordinates(endCellParentElem, false, true);
21 ilm 367
                        if (coordinates == null)
368
                            throw new IllegalStateException("Couldn't find the height of the shape : " + JDOMUtils.output(endCellParentElem));
93 ilm 369
                        final Length endYFromAnchor = coordinates[3];
21 ilm 370
                        assert endYFromAnchor != null : "getCoordinates() should never return null BigDecimal (unless requested by horizontal/vertical)";
371
                        // find the end row
372
                        int rowIndex = startRowIndex;
93 ilm 373
                        Length cellEndYFromAnchor = getRow(rowIndex).getStyle().getTableRowProperties().getHeight();
21 ilm 374
                        while (endYFromAnchor.compareTo(cellEndYFromAnchor) > 0) {
375
                            rowIndex++;
93 ilm 376
                            cellEndYFromAnchor = cellEndYFromAnchor.add(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
21 ilm 377
                        }
378
                        // find the end-y
93 ilm 379
                        final Length cellStartYFromAnchor = cellEndYFromAnchor.subtract(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
380
                        final Length endY = endYFromAnchor.subtract(cellStartYFromAnchor);
21 ilm 381
                        assert endY.signum() >= 0;
382
 
383
                        newEndY = rowIndex;
93 ilm 384
                        endCellParentElem.setAttribute("end-y", endY.format(), getTABLE());
19 ilm 385
                    }
21 ilm 386
                    endCellAttr.setValue(SpreadSheet.formatSheetName(endCellSheet.getName()) + "." + Table.getAddress(new Point(endCellPoint.x, newEndY)));
19 ilm 387
                }
388
            }
389
        }
17 ilm 390
    }
391
 
25 ilm 392
    private synchronized void addRow(Element child, StyleDesc<RowStyle> styleDesc, StyleDesc<CellStyle> cellStyleDesc) {
393
        final Row<D> row = new Row<D>(this, child, this.rows.size(), styleDesc, cellStyleDesc);
394
        final int toRepeat = row.getRepeated();
395
        for (int i = 0; i < toRepeat; i++) {
396
            this.rows.add(row);
397
        }
17 ilm 398
    }
399
 
400
    public final Point resolveHint(String ref) {
401
        final Point res = resolve(ref);
402
        if (res != null) {
403
            return res;
404
        } else
405
            throw new IllegalArgumentException(ref + " is not a cell ref, if it's a named range, you must use it on a SpreadSheet.");
406
    }
407
 
408
    // *** set cell
409
 
410
    public final boolean isCellValid(int x, int y) {
411
        if (x > this.getColumnCount())
412
            return false;
413
        else if (y > this.getRowCount())
414
            return false;
415
        else
416
            return this.getImmutableCellAt(x, y).isValid();
417
    }
418
 
25 ilm 419
    /**
420
     * Return a modifiable cell at the passed coordinates. This is slower than
421
     * {@link #getImmutableCellAt(int, int)} since this method may modify the underlying XML (e.g.
422
     * break up repeated cells to allow for modification of only the returned cell).
423
     *
424
     * @param x the column.
425
     * @param y the row.
426
     * @return the cell.
427
     * @see #getImmutableCellAt(int, int)
428
     */
17 ilm 429
    public final MutableCell<D> getCellAt(int x, int y) {
156 ilm 430
        final Row<D> r = this.getMutableRow(y);
431
        try {
432
            return r.getMutableCellAt(x);
433
        } catch (Exception e) {
434
            throw new IllegalArgumentException("Couldn't get mutable cell at " + getAddress(x, y), e);
435
        }
17 ilm 436
    }
437
 
438
    public final MutableCell<D> getCellAt(String ref) {
439
        return this.getCellAt(resolveHint(ref));
440
    }
441
 
442
    final MutableCell<D> getCellAt(Point p) {
443
        return this.getCellAt(p.x, p.y);
444
    }
445
 
446
    /**
447
     * Sets the value at the specified coordinates.
448
     *
449
     * @param val the new value, <code>null</code> will be treated as "".
450
     * @param x the column.
451
     * @param y the row.
452
     */
453
    public final void setValueAt(Object val, int x, int y) {
454
        if (val == null)
455
            val = "";
456
        // ne pas casser les repeated pour rien
457
        if (!val.equals(this.getValueAt(x, y)))
458
            this.getCellAt(x, y).setValue(val);
459
    }
460
 
461
    // *** get cell
462
 
25 ilm 463
    /**
464
     * Return a non modifiable cell at the passed coordinates. This is faster than
465
     * {@link #getCellAt(int, int)} since this method never modifies the underlying XML.
466
     *
467
     * @param x the column.
468
     * @param y the row.
469
     * @return the cell.
470
     * @see #getCellAt(int, int)
471
     */
472
    public final Cell<D> getImmutableCellAt(int x, int y) {
17 ilm 473
        return this.getRow(y).getCellAt(x);
474
    }
475
 
25 ilm 476
    public final Cell<D> getImmutableCellAt(String ref) {
17 ilm 477
        final Point p = resolveHint(ref);
478
        return this.getImmutableCellAt(p.x, p.y);
479
    }
480
 
481
    /**
21 ilm 482
     * Return the origin of a merged cell.
483
     *
484
     * @param x the column.
485
     * @param y the row.
486
     * @return the point of origin, <code>null</code> if there's no merged cell at the passed
487
     *         coordinates.
488
     */
489
    public final Point getCoverOrigin(final int x, final int y) {
490
        // can't return a Cell, since it has no x
491
        // don't return a MutableCell since it is costly
492
 
493
        final Cell<D> c = this.getImmutableCellAt(x, y);
494
        if (c.coversOtherCells()) {
495
            return new Point(x, y);
496
        } else if (!c.isCovered()) {
497
            return null;
498
        } else {
499
            final Row<D> row = this.getRow(y);
500
            Cell<D> currentCell = c;
501
            int currentX = x;
502
            while (currentX > 0 && currentCell.isCovered()) {
503
                currentX--;
504
                currentCell = row.getCellAt(currentX);
505
            }
506
            if (currentCell.coversOtherCells())
507
                return new Point(currentX, y);
508
 
509
            if (!currentCell.isCovered()) {
510
                currentX++;
511
                currentCell = row.getCellAt(currentX);
512
            }
513
            assert currentCell.isCovered();
514
 
515
            int currentY = y;
516
            while (!currentCell.coversOtherCells()) {
517
                currentY--;
518
                currentCell = this.getImmutableCellAt(currentX, currentY);
519
            }
520
            return new Point(currentX, currentY);
521
        }
522
    }
523
 
524
    /**
80 ilm 525
     * The value of the cell at the passed coordinates.
526
     *
527
     * @param row row index from <code>0</code> to <code>{@link #getRowCount() row count}-1</code>
528
     * @param column column index from <code>0</code> to
529
     *        <code>{@link #getColumnCount() column count}-1</code>
530
     * @return the value at the passed coordinates.
531
     * @see #getValueAt(String)
532
     * @see Cell#getValue()
17 ilm 533
     */
534
    public final Object getValueAt(int column, int row) {
535
        return this.getImmutableCellAt(column, row).getValue();
536
    }
537
 
538
    /**
539
     * Find the style name for the specified cell.
540
     *
541
     * @param column column index.
542
     * @param row row index.
543
     * @return the style name, can be <code>null</code>.
544
     */
545
    public final String getStyleNameAt(int column, int row) {
546
        // first the cell
547
        String cellStyle = this.getImmutableCellAt(column, row).getStyleAttr();
548
        if (cellStyle != null)
549
            return cellStyle;
550
        // then the row (as specified in §2 of section 8.1)
73 ilm 551
        // in reality LO never generates table-row/@default-cell-style-name and its behavior is
552
        // really unpredictable if it opens files with it
67 ilm 553
        if (Style.isStandardStyleResolution()) {
554
            cellStyle = this.getRow(row).getElement().getAttributeValue("default-cell-style-name", getTABLE());
555
            if (cellStyle != null)
556
                return cellStyle;
557
        }
17 ilm 558
        // and finally the column
559
        return this.getColumn(column).getElement().getAttributeValue("default-cell-style-name", getTABLE());
560
    }
561
 
562
    public final CellStyle getStyleAt(int column, int row) {
25 ilm 563
        return getCellStyleDesc().findStyleForNode(this.getImmutableCellAt(column, row), this.getStyleNameAt(column, row));
17 ilm 564
    }
565
 
73 ilm 566
    public final StyleTableCellProperties getTableCellPropertiesAt(int column, int row) {
567
        final CellStyle styleAt = this.getStyleAt(column, row);
568
        return styleAt == null ? null : styleAt.getTableCellProperties(this.getImmutableCellAt(column, row));
569
    }
570
 
20 ilm 571
    protected StyleStyleDesc<CellStyle> getCellStyleDesc() {
572
        return Style.getStyleStyleDesc(CellStyle.class, getODDocument().getVersion());
573
    }
574
 
575
    public final CellStyle getDefaultCellStyle() {
576
        return getCellStyleDesc().findDefaultStyle(this.getODDocument().getPackage());
577
    }
578
 
17 ilm 579
    /**
580
     * Return the coordinates of cells using the passed style.
581
     *
582
     * @param cellStyleName a style name.
583
     * @return the cells using <code>cellStyleName</code>.
584
     */
585
    public final List<Tuple2<Integer, Integer>> getStyleReferences(final String cellStyleName) {
586
        final List<Tuple2<Integer, Integer>> res = new ArrayList<Tuple2<Integer, Integer>>();
587
        final Set<Integer> cols = new HashSet<Integer>();
588
        final int columnCount = getColumnCount();
589
        for (int i = 0; i < columnCount; i++) {
590
            if (cellStyleName.equals(this.getColumn(i).getElement().getAttributeValue("default-cell-style-name", getTABLE())))
591
                cols.add(i);
592
        }
593
        final int rowCount = getRowCount();
594
        for (int y = 0; y < rowCount; y++) {
595
            final Row<D> row = this.getRow(y);
596
            final String rowStyle = row.getElement().getAttributeValue("default-cell-style-name", getTABLE());
597
            for (int x = 0; x < columnCount; x++) {
598
                final String cellStyle = row.getCellAt(x).getStyleAttr();
599
                final boolean match;
600
                // first the cell
601
                if (cellStyle != null)
602
                    match = cellStyleName.equals(cellStyle);
603
                // then the row (as specified in §2 of section 8.1)
604
                else if (rowStyle != null)
605
                    match = cellStyleName.equals(rowStyle);
606
                // and finally the column
607
                else
608
                    match = cols.contains(x);
609
 
610
                if (match)
611
                    res.add(Tuple2.create(x, y));
612
            }
613
        }
614
        return res;
615
    }
616
 
25 ilm 617
    protected final StyleStyleDesc<ColumnStyle> getColumnStyleDesc() {
618
        return Style.getStyleStyleDesc(ColumnStyle.class, XMLVersion.getVersion(getElement()));
619
    }
620
 
621
    protected final StyleStyleDesc<RowStyle> getRowStyleDesc() {
622
        return Style.getStyleStyleDesc(RowStyle.class, XMLVersion.getVersion(getElement()));
623
    }
624
 
17 ilm 625
    /**
80 ilm 626
     * The value of the cell at the passed coordinates.
17 ilm 627
     *
80 ilm 628
     * @param ref a reference, e.g. "A3".
629
     * @return the value at the passed coordinates.
630
     * @see #getValueAt(int, int)
631
     * @see Cell#getValue()
17 ilm 632
     */
633
    public final Object getValueAt(String ref) {
634
        return this.getImmutableCellAt(ref).getValue();
635
    }
636
 
637
    // *** get count
638
 
93 ilm 639
    // not public since Row represent the physical (XML) row, i.e. it can represent many logical
640
    // rows. Ideally two classes should be created (as Cell/MutableCell), but this would only be
641
    // useful for changing style (e.g. setHeight()).
642
 
25 ilm 643
    final Row<D> getRow(int index) {
17 ilm 644
        return this.rows.get(index);
645
    }
646
 
25 ilm 647
    final Row<D> getMutableRow(int y) {
648
        final Row<D> c = this.getRow(y);
649
        if (c.getRepeated() > 1) {
650
            RepeatedBreaker.<D> getRowBreaker().breakRepeated(this, this.rows, y);
651
            return this.getRow(y);
652
        } else {
653
            return c;
654
        }
655
    }
656
 
93 ilm 657
    // OK to use immutable row since for now RowStyle has no setter (except for raw methods in super
658
    // classes)
659
    public final RowStyle getRowStyle(int index) {
660
        return this.getRow(index).getStyle();
661
    }
662
 
17 ilm 663
    public final Column<D> getColumn(int i) {
664
        return this.cols.get(i);
665
    }
666
 
93 ilm 667
    public final ColumnStyle getColumnStyle(int index) {
668
        return this.getColumn(index).getStyle();
669
    }
670
 
17 ilm 671
    public final int getRowCount() {
672
        return this.rows.size();
673
    }
674
 
25 ilm 675
    public final TableGroup getRowGroup() {
676
        return this.rowGroup;
677
    }
678
 
679
    /**
680
     * Return the deepest group at the passed row.
681
     *
682
     * @param y a row index.
683
     * @return the group at the index, never <code>null</code>.
684
     */
685
    public final TableGroup getRowGroupAt(final int y) {
686
        return this.getRowGroup().getDescendentOrSelfContaining(y);
687
    }
688
 
17 ilm 689
    public final int getHeaderRowCount() {
25 ilm 690
        return this.getRowGroup().getFollowingHeaderCount();
17 ilm 691
    }
692
 
693
    public final int getColumnCount() {
694
        return this.cols.size();
695
    }
696
 
25 ilm 697
    public final TableGroup getColumnGroup() {
698
        return this.columnGroup;
699
    }
700
 
701
    /**
702
     * Return the deepest group at the passed column.
703
     *
704
     * @param x a column index.
705
     * @return the group at the index, never <code>null</code>.
706
     */
707
    public final TableGroup getColumnGroupAt(final int x) {
708
        return this.getColumnGroup().getDescendentOrSelfContaining(x);
709
    }
710
 
17 ilm 711
    public final int getHeaderColumnCount() {
25 ilm 712
        return this.getColumnGroup().getFollowingHeaderCount();
17 ilm 713
    }
714
 
715
    // *** set count
716
 
717
    /**
718
     * Changes the column count without keeping the table width.
719
     *
720
     * @param newSize the new column count.
721
     * @see #setColumnCount(int, int, boolean)
722
     */
723
    public final void setColumnCount(int newSize) {
724
        this.setColumnCount(newSize, -1, false);
725
    }
726
 
727
    /**
728
     * Assure that this sheet has at least <code>newSize</code> columns.
729
     *
730
     * @param newSize the minimum column count this table should have.
731
     */
732
    public final void ensureColumnCount(int newSize) {
733
        if (newSize > this.getColumnCount())
734
            this.setColumnCount(newSize);
735
    }
736
 
737
    /**
738
     * Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
739
     * extra cells will be chopped off. Otherwise empty cells will be created.
740
     *
741
     * @param newSize the new column count.
73 ilm 742
     * @param colIndex the index of the column to be copied, -1 for empty column (and no style).
17 ilm 743
     * @param keepTableWidth <code>true</code> if the table should be same width after the column
744
     *        change.
745
     */
746
    public final void setColumnCount(int newSize, int colIndex, final boolean keepTableWidth) {
73 ilm 747
        this.setColumnCount(newSize, colIndex, null, keepTableWidth);
748
    }
749
 
750
    /**
751
     * Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
752
     * extra cells will be chopped off. Otherwise empty cells will be created.
753
     *
754
     * @param newSize the new column count.
755
     * @param colStyle the style of the new columns, <code>null</code> for no style (throws
756
     *        exception if the table has a non-null width).
757
     * @param keepTableWidth <code>true</code> if the table should be same width after the column
758
     *        change.
759
     * @see #createColumnStyle(Number, LengthUnit)
760
     */
761
    public final void setColumnCount(int newSize, ColumnStyle colStyle, final boolean keepTableWidth) {
762
        this.setColumnCount(newSize, -1, colStyle, keepTableWidth);
763
    }
764
 
765
    private final void setColumnCount(int newSize, int colIndex, ColumnStyle colStyle, final boolean keepTableWidth) {
17 ilm 766
        final int toGrow = newSize - this.getColumnCount();
767
        if (toGrow < 0) {
768
            this.removeColumn(newSize, this.getColumnCount(), keepTableWidth);
769
        } else if (toGrow > 0) {
770
            // the list of columns cannot be mixed with other elements
771
            // so just keep adding after the last one
772
            final int indexOfLastCol;
773
            if (this.getColumnCount() == 0)
774
                // from section 8.1.1 the only possible elements after cols are rows
775
                // but there can't be rows w/o columns, so just add to the end
776
                indexOfLastCol = this.getElement().getContentSize() - 1;
777
            else
778
                indexOfLastCol = this.getElement().getContent().indexOf(this.getColumn(this.getColumnCount() - 1).getElement());
779
 
780
            final Element elemToClone;
781
            if (colIndex < 0) {
73 ilm 782
                elemToClone = Column.createEmpty(getODDocument().getVersion(), colStyle);
17 ilm 783
            } else {
784
                elemToClone = getColumn(colIndex).getElement();
785
            }
25 ilm 786
            final StyleStyleDesc<ColumnStyle> columnStyleDesc = getColumnStyleDesc();
17 ilm 787
            for (int i = 0; i < toGrow; i++) {
788
                final Element newElem = (Element) elemToClone.clone();
789
                this.getElement().addContent(indexOfLastCol + 1 + i, newElem);
25 ilm 790
                this.cols.add(new Column<D>(this, newElem, columnStyleDesc));
17 ilm 791
            }
792
            // now update widths
793
            updateWidth(keepTableWidth);
794
 
795
            // add needed cells
25 ilm 796
            final StyleStyleDesc<CellStyle> cellStyleDesc = this.getCellStyleDesc();
797
            final int rowCount = this.getRowCount();
798
            for (int i = 0; i < rowCount;) {
799
                final Row<D> r = this.getRow(i);
800
                r.columnCountChanged(cellStyleDesc);
801
                i += r.getRepeated();
17 ilm 802
            }
803
        }
804
    }
805
 
806
    public final void removeColumn(int colIndex, final boolean keepTableWidth) {
807
        this.removeColumn(colIndex, colIndex + 1, keepTableWidth);
808
    }
809
 
810
    /**
811
     * Remove columns from this. As with OpenOffice, no cell must be covered in the column to
812
     * remove. ATTN <code>keepTableWidth</code> only works for tables in text document that are not
813
     * aligned automatically (ie fill the entire page). ATTN spreadsheet applications may hide from
814
     * you the real width of sheets, eg display only columns A to AJ when in reality there's
815
     * hundreds of blank columns beyond. Thus if you pass <code>true</code> to
816
     * <code>keepTableWidth</code> you'll end up with huge widths.
817
     *
818
     * @param firstIndex the first column to remove.
819
     * @param lastIndex the last column to remove, exclusive.
820
     * @param keepTableWidth <code>true</code> if the table should be same width after the column
821
     *        change.
822
     */
823
    public final void removeColumn(int firstIndex, int lastIndex, final boolean keepTableWidth) {
824
        // first check that removeCells() will succeed, so that we avoid an incoherent XML state
25 ilm 825
        final int rowCount = this.getRowCount();
826
        for (int i = 0; i < rowCount;) {
827
            final Row<D> r = this.getRow(i);
17 ilm 828
            r.checkRemove(firstIndex, lastIndex);
25 ilm 829
            i += r.getRepeated();
17 ilm 830
        }
831
        // rm column element
25 ilm 832
        remove(Axis.COLUMN, firstIndex, lastIndex - 1);
17 ilm 833
        // update widths
834
        updateWidth(keepTableWidth);
835
        // rm cells
25 ilm 836
        for (int i = 0; i < rowCount;) {
837
            final Row<D> r = this.getRow(i);
17 ilm 838
            r.removeCells(firstIndex, lastIndex);
25 ilm 839
            i += r.getRepeated();
17 ilm 840
        }
841
    }
842
 
80 ilm 843
    void updateWidth(final boolean keepTableWidth) {
844
        // tables widths must have a lower precision than columns since they can't be the exact sum
845
        // of them.
846
        final Length currentWidth = getWidth().roundDecimalAmount();
847
        Length newWidth = Length.ZERO;
17 ilm 848
        Column<?> nullWidthCol = null;
849
        // columns are flattened in ctor: no repeated
850
        for (final Column<?> col : this.cols) {
80 ilm 851
            final Length colWidth = col.getWidth();
852
            if (colWidth.isDefined()) {
853
                assert colWidth.signum() >= 0;
854
                newWidth = newWidth.add(colWidth);
17 ilm 855
            } else {
856
                // we cannot compute the newWidth
80 ilm 857
                newWidth = Length.getNone();
17 ilm 858
                nullWidthCol = col;
859
                break;
860
            }
861
        }
80 ilm 862
        // tables widths must have a lower precision than columns since they can't be the exact sum
863
        // of them.
864
        newWidth = newWidth.roundDecimalAmount();
17 ilm 865
        // remove all rel-column-width, simpler and Spreadsheet doesn't use them
866
        // SpreadSheets have no table width
80 ilm 867
        if (keepTableWidth && currentWidth.isDefined()) {
17 ilm 868
            if (nullWidthCol != null)
869
                throw new IllegalStateException("Cannot keep width since a column has no width : " + nullWidthCol);
870
            // compute column-width from table width
80 ilm 871
            final Number ratio = currentWidth.divide(newWidth);
17 ilm 872
            // once per style not once per col, otherwise if multiple columns with same styles they
873
            // all will be affected multiple times
874
            final Set<ColumnStyle> colStyles = new HashSet<ColumnStyle>();
875
            for (final Column<?> col : this.cols) {
876
                colStyles.add(col.getStyle());
877
            }
878
            for (final ColumnStyle colStyle : colStyles) {
80 ilm 879
                colStyle.setWidth(colStyle.getWidth().multiply(ratio));
17 ilm 880
            }
881
        } else {
882
            // compute table width from column-width
883
            final TableStyle style = this.getStyle();
884
            if (style != null) {
73 ilm 885
                if (nullWidthCol == null)
886
                    style.setWidth(newWidth);
80 ilm 887
                else if (currentWidth.isDefined())
17 ilm 888
                    throw new IllegalStateException("Cannot update table width since a column has no width : " + nullWidthCol);
80 ilm 889
                // else currentWidth undefined, no inconsistency, nothing to update
17 ilm 890
            }
73 ilm 891
            // either there's no table width or it's positive and equal to the sum of its columns
80 ilm 892
            assert style == null || style.getWidth().isNone() || (!newWidth.isNone() && newWidth.compareTo(style.getWidth()) == 0);
17 ilm 893
            for (final Column<?> col : this.cols) {
894
                final ColumnStyle colStyle = col.getStyle();
895
                // if no style, nothing to remove
896
                if (colStyle != null)
897
                    colStyle.rmRelWidth();
898
            }
899
        }
900
    }
901
 
902
    /**
903
     * Table width.
904
     *
905
     * @return the table width, can be <code>null</code> (table has no style or style has no width,
906
     *         eg in SpreadSheet).
907
     */
80 ilm 908
    public final Length getWidth() {
909
        return this.getStyle().getWidth();
17 ilm 910
    }
911
 
73 ilm 912
    /**
913
     * Create and add an automatic style.
914
     *
915
     * @param amount the column width.
916
     * @param unit the unit of <code>amount</code>.
917
     * @return the newly added style.
918
     */
919
    public final ColumnStyle createColumnStyle(final Number amount, final LengthUnit unit) {
80 ilm 920
        return createColumnStyle(amount == null ? Length.getNone() : Length.create(amount, unit));
921
    }
922
 
923
    public final ColumnStyle createColumnStyle(final Length l) {
73 ilm 924
        final ColumnStyle colStyle = this.getStyleDesc(ColumnStyle.class).createAutoStyle(this.getODDocument().getPackage());
80 ilm 925
        colStyle.setWidth(l);
17 ilm 926
        return colStyle;
927
    }
928
 
25 ilm 929
    private final void setCount(final Axis col, final int newSize) {
17 ilm 930
        this.remove(col, newSize, -1);
931
    }
932
 
73 ilm 933
    private final void remove(final Axis col, final int fromIndex, final int toIndexIncl) {
934
        this.remove(col, fromIndex, toIndexIncl, true);
935
    }
936
 
17 ilm 937
    // both inclusive
73 ilm 938
    private final void remove(final Axis col, final int fromIndex, final int toIndexIncl, final boolean updateList) {
939
        assert col == Axis.COLUMN || !updateList || toIndexIncl < 0 : "Row index will be wrong";
25 ilm 940
        final List<? extends TableCalcNode<?, ?>> l = col == Axis.COLUMN ? this.cols : this.rows;
17 ilm 941
        final int toIndexValid = CollectionUtils.getValidIndex(l, toIndexIncl);
25 ilm 942
        final int toRemoveCount = toIndexValid - fromIndex + 1;
943
        int removedCount = 0;
944
        while (removedCount < toRemoveCount) {
945
            // works backwards to keep y OK
946
            final int i = toIndexValid - removedCount;
947
            final TableCalcNode<?, ?> removed = l.get(i);
948
            if (removed instanceof Row) {
949
                final Row<?> r = (Row<?>) removed;
950
                final int removeFromRepeated = i - Math.max(fromIndex, r.getY()) + 1;
951
                // removedCount grows each iteration
952
                assert removeFromRepeated > 0;
953
                final int newRepeated = r.getRepeated() - removeFromRepeated;
954
                if (newRepeated == 0)
955
                    removed.getElement().detach();
956
                else
957
                    r.setRepeated(newRepeated);
958
                removedCount += removeFromRepeated;
959
            } else {
960
                // Columns are always flattened
961
                removed.getElement().detach();
962
                removedCount++;
963
            }
17 ilm 964
        }
25 ilm 965
        // one remove to be efficient
73 ilm 966
        if (updateList)
967
            l.subList(fromIndex, toIndexValid + 1).clear();
17 ilm 968
    }
969
 
970
    public final void ensureRowCount(int newSize) {
971
        if (newSize > this.getRowCount())
972
            this.setRowCount(newSize);
973
    }
974
 
975
    public final void setRowCount(int newSize) {
976
        this.setRowCount(newSize, -1);
977
    }
978
 
979
    /**
980
     * Changes the row count. If <code>newSize</code> is less than {@link #getRowCount()} extra rows
981
     * will be chopped off. Otherwise empty cells will be created.
982
     *
983
     * @param newSize the new row count.
984
     * @param rowIndex the index of the row to be copied, -1 for empty row (i.e. default style).
985
     */
986
    public final void setRowCount(int newSize, int rowIndex) {
987
        final int toGrow = newSize - this.getRowCount();
988
        if (toGrow < 0) {
25 ilm 989
            setCount(Axis.ROW, newSize);
990
        } else if (toGrow > 0) {
991
            final Element elemToClone;
992
            if (rowIndex < 0) {
993
                elemToClone = Row.createEmpty(this.getODDocument().getVersion());
994
                // each row MUST have the same number of columns
995
                elemToClone.addContent(Cell.createEmpty(this.getODDocument().getVersion(), this.getColumnCount()));
996
            } else {
997
                elemToClone = (Element) getRow(rowIndex).getElement().clone();
17 ilm 998
            }
25 ilm 999
            Axis.ROW.setRepeated(elemToClone, toGrow);
1000
            // as per section 8.1.1 rows are the last elements inside a table
1001
            this.getElement().addContent(elemToClone);
1002
            addRow(elemToClone, getRowStyleDesc(), getCellStyleDesc());
17 ilm 1003
        }
1004
    }
1005
 
73 ilm 1006
    public final void removeRow(int index) {
1007
        this.removeRows(index, index + 1);
1008
    }
1009
 
1010
    /**
1011
     * Remove rows between the two passed indexes. If the origin of a merged cell is contained in
1012
     * the rows to remove, then it will be unmerged, otherwise (the origin is before the rows) the
1013
     * merged cell will just be shrunk.
1014
     *
1015
     * @param firstIndex the start index, inclusive.
1016
     * @param lastIndex the stop index, exclusive.
1017
     */
1018
    public final void removeRows(int firstIndex, int lastIndex) {
1019
        if (firstIndex < 0)
1020
            throw new IllegalArgumentException("Negative index " + firstIndex);
1021
        if (firstIndex == lastIndex)
1022
            return;
1023
        if (firstIndex > lastIndex)
1024
            throw new IllegalArgumentException("First index after last index " + firstIndex + " > " + lastIndex);
1025
 
1026
        // remove row elements, minding merged cells
1027
        final int columnCount = getColumnCount();
1028
        final Set<Point> coverOrigins = new HashSet<Point>();
1029
        for (int y = firstIndex; y < lastIndex; y++) {
1030
            for (int x = 0; x < columnCount; x++) {
1031
                final Cell<D> cell = this.getImmutableCellAt(x, y);
1032
                if (cell.isCovered()) {
1033
                    coverOrigins.add(this.getCoverOrigin(x, y));
1034
                    // cells spanning inside the removed rows don't need to be unmerged
1035
                } else if (cell.coversOtherCells() && y + cell.getRowsSpanned() > lastIndex) {
1036
                    this.getCellAt(x, y).unmerge();
1037
                }
1038
            }
1039
        }
1040
 
1041
        // don't update this.rows as we need them below and indexes would be wrong anyway
1042
        this.remove(Axis.ROW, firstIndex, lastIndex - 1, false);
1043
 
1044
        // adjust rows spanned
1045
        // we haven't yet reset this.rows, so we can use getImmutableCellAt()
1046
        for (final Point coverOrigin : coverOrigins) {
1047
            // if the origin is inside the removed rows, then the whole merged cell was removed
1048
            // (if the merged cell extended past lastIndex, it got unmerged above and wasn't added
1049
            // to the set)
1050
            if (coverOrigin.y >= firstIndex) {
1051
                assert this.getImmutableCellAt(coverOrigin.x, coverOrigin.y).getElement().getDocument() == null;
1052
                continue;
1053
            }
1054
            final Cell<D> coverOriginCell = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
1055
            final int rowsSpanned = coverOriginCell.getRowsSpanned();
1056
            // otherwise the cover origin is between firstIndex and lastIndex and should have been
1057
            // unmerged
1058
            assert rowsSpanned > 1;
1059
            final int stopY = coverOrigin.y + rowsSpanned;
1060
            final int toRemove = Math.min(lastIndex, stopY) - firstIndex;
1061
            assert toRemove > 0 && toRemove < rowsSpanned;
1062
            coverOriginCell.getElement().setAttribute("number-rows-spanned", (rowsSpanned - toRemove) + "", getTABLE());
1063
        }
1064
 
1065
        // remove empty table-row-group
1066
        // since indexes are stored in TableGroup, we can do it after having removed rows
1067
        final Set<TableGroup> groups = new HashSet<TableGroup>();
1068
        for (int y = firstIndex; y < lastIndex; y++) {
1069
            groups.add(this.getRowGroupAt(y));
1070
        }
1071
        for (final TableGroup group : groups) {
1072
            TableGroup g = group;
1073
            Element elem = g.getElement();
1074
            while (g != null && elem.getParent() != null && elem.getChildren().size() == 0) {
1075
                elem.detach();
1076
                g = g.getParent();
1077
                elem = g == null ? null : g.getElement();
1078
            }
1079
        }
1080
 
1081
        // recreate list from XML
1082
        this.readRows();
1083
    }
1084
 
17 ilm 1085
    // *** table models
1086
 
1087
    public final SheetTableModel<D> getTableModel(final int column, final int row) {
1088
        return new SheetTableModel<D>(this, row, column);
1089
    }
1090
 
1091
    public final SheetTableModel<D> getTableModel(final int column, final int row, final int lastCol, final int lastRow) {
1092
        return new SheetTableModel<D>(this, row, column, lastRow, lastCol);
1093
    }
1094
 
1095
    public final MutableTableModel<D> getMutableTableModel(final int column, final int row) {
1096
        return new MutableTableModel<D>(this, row, column);
1097
    }
1098
 
1099
    /**
1100
     * Return the table from <code>start</code> to <code>end</code> inclusive.
1101
     *
1102
     * @param start the first cell of the result.
1103
     * @param end the last cell of the result.
1104
     * @return the table.
1105
     */
1106
    public final MutableTableModel<D> getMutableTableModel(final Point start, final Point end) {
1107
        // +1 since exclusive
1108
        return new MutableTableModel<D>(this, start.y, start.x, end.y + 1, end.x + 1);
1109
    }
1110
 
1111
    public final void merge(TableModel t, final int column, final int row) {
1112
        this.merge(t, column, row, false);
1113
    }
1114
 
1115
    /**
1116
     * Merges t into this sheet at the specified point.
1117
     *
1118
     * @param t the data to be merged.
1119
     * @param column the column t will be merged at.
1120
     * @param row the row t will be merged at.
1121
     * @param includeColNames if <code>true</code> the column names of t will also be merged.
1122
     */
1123
    public final void merge(TableModel t, final int column, final int row, final boolean includeColNames) {
1124
        final int offset = (includeColNames ? 1 : 0);
1125
        // the columns must be first, see section 8.1.1 of v1.1
1126
        this.ensureColumnCount(column + t.getColumnCount());
1127
        this.ensureRowCount(row + t.getRowCount() + offset);
1128
        final TableModel thisModel = this.getMutableTableModel(column, row);
1129
        if (includeColNames) {
1130
            for (int x = 0; x < t.getColumnCount(); x++) {
1131
                thisModel.setValueAt(t.getColumnName(x), 0, x);
1132
            }
1133
        }
1134
        for (int y = 0; y < t.getRowCount(); y++) {
1135
            for (int x = 0; x < t.getColumnCount(); x++) {
1136
                final Object value = t.getValueAt(y, x);
1137
                thisModel.setValueAt(value, y + offset, x);
1138
            }
1139
        }
1140
    }
1141
 
73 ilm 1142
    /**
1143
     * All ranges defined in this table.
1144
     *
1145
     * @return the names.
1146
     * @see SpreadSheet#getRangesNames()
1147
     */
1148
    public final Set<String> getRangesNames() {
1149
        return SpreadSheet.getRangesNames(getElement(), getTABLE());
1150
    }
1151
 
1152
    /**
1153
     * Get a named range in this table.
1154
     *
1155
     * @param name the name of the range.
1156
     * @return a named range, or <code>null</code> if the passed name doesn't exist.
1157
     * @see #getRangesNames()
1158
     * @see SpreadSheet#getRange(String)
1159
     */
1160
    public final Range getRange(String name) {
1161
        return SpreadSheet.getRange(getElement(), getTABLE(), name);
1162
    }
1163
 
1164
    public final MutableTableModel<D> getTableModel(String name) {
1165
        final Range points = getRange(name);
1166
        if (points == null)
1167
            return null;
1168
        if (points.spanSheets())
1169
            throw new IllegalStateException("different sheet names: " + points.getStartSheet() + " != " + points.getEndSheet());
1170
        return this.getMutableTableModel(points.getStartPoint(), points.getEndPoint());
1171
    }
1172
 
17 ilm 1173
    // * UsedRange & CurrentRegion
1174
 
1175
    /**
1176
     * The range that covers all used cells.
1177
     *
1178
     * @return the range that covers all used cells, <code>null</code> if the table is completely
1179
     *         empty.
1180
     */
1181
    public final Range getUsedRange() {
1182
        return this.getUsedRange(false);
1183
    }
1184
 
1185
    /**
1186
     * The range that covers all used cells.
1187
     *
1188
     * @param checkStyle <code>true</code> to check the background and borders in addition to the
1189
     *        content.
1190
     * @return the range that covers all used cells, <code>null</code> if the table is completely
1191
     *         blank.
1192
     */
1193
    public final Range getUsedRange(boolean checkStyle) {
1194
        int minX = -1, minY = -1, maxX = -1, maxY = -1;
1195
        final int colCount = this.getColumnCount();
1196
        final int rowCount = this.getRowCount();
1197
        for (int x = 0; x < colCount; x++) {
1198
            for (int y = 0; y < rowCount; y++) {
1199
                if (!this.isCellBlank(x, y, checkStyle)) {
1200
                    if (minX < 0 || x < minX)
1201
                        minX = x;
1202
                    if (minY < 0 || y < minY)
1203
                        minY = y;
1204
 
1205
                    if (maxX < 0 || x > maxX)
1206
                        maxX = x;
1207
                    if (maxY < 0 || y > maxY)
1208
                        maxY = y;
1209
                }
1210
            }
1211
        }
1212
        return minX < 0 ? null : new Range(getName(), new Point(minX, minY), new Point(maxX, maxY));
1213
    }
1214
 
1215
    protected final boolean isCellBlank(final int x, int y, boolean checkStyle) {
1216
        if (!getImmutableCellAt(x, y).isEmpty())
1217
            return false;
1218
 
1219
        if (checkStyle) {
73 ilm 1220
            final StyleTableCellProperties tableCellProperties = this.getTableCellPropertiesAt(x, y);
1221
            return tableCellProperties == null || (tableCellProperties.getBackgroundColor() == null && tableCellProperties.getBorders().isEmpty());
17 ilm 1222
        } else {
1223
            return true;
1224
        }
1225
    }
1226
 
1227
    private class RegionExplorer {
1228
 
1229
        private final boolean checkStyle;
1230
        private final int rowCount, colCount;
1231
        protected int minX, minY, maxX, maxY;
1232
 
1233
        public RegionExplorer(final int startX, final int startY, final boolean checkStyle) {
1234
            this.rowCount = getRowCount();
1235
            this.colCount = getColumnCount();
1236
            this.minX = this.maxX = startX;
1237
            this.minY = this.maxY = startY;
1238
            this.checkStyle = checkStyle;
1239
        }
1240
 
1241
        public boolean canXDecrement() {
1242
            return this.minX > 0;
1243
        }
1244
 
1245
        public boolean canYDecrement() {
1246
            return this.minY > 0;
1247
        }
1248
 
1249
        public boolean canXIncrement() {
1250
            return this.maxX < this.colCount - 1;
1251
        }
1252
 
1253
        public boolean canYIncrement() {
1254
            return this.maxY < this.rowCount - 1;
1255
        }
1256
 
1257
        private boolean checkRow(final boolean upper) {
1258
            if (upper && this.canYDecrement() || !upper && this.canYIncrement()) {
1259
                final int y = upper ? this.minY - 1 : this.maxY + 1;
1260
                final int start = this.canXDecrement() ? this.minX - 1 : this.minX;
1261
                final int stop = this.canXIncrement() ? this.maxX + 1 : this.maxX;
1262
                for (int x = start; x <= stop; x++) {
1263
                    if (!isCellBlank(x, y, this.checkStyle)) {
1264
                        if (upper)
1265
                            this.minY = y;
1266
                        else
1267
                            this.maxY = y;
1268
                        if (x < this.minX)
1269
                            this.minX = x;
1270
                        if (x > this.maxX)
1271
                            this.maxX = x;
1272
                        return true;
1273
                    }
1274
                }
1275
            }
1276
            return false;
1277
        }
1278
 
1279
        // sans corners (checked by checkRow())
1280
        private boolean checkCol(final boolean left) {
1281
            if (left && this.canXDecrement() || !left && this.canXIncrement()) {
1282
                final int x = left ? this.minX - 1 : this.maxX + 1;
1283
                for (int y = this.minY; y <= this.maxY; y++) {
1284
                    if (!isCellBlank(x, y, this.checkStyle)) {
1285
                        if (left)
1286
                            this.minX = x;
1287
                        else
1288
                            this.maxX = x;
1289
                        return true;
1290
                    }
1291
                }
1292
            }
1293
            return false;
1294
        }
1295
 
1296
        private final boolean checkFrame() {
1297
            return this.checkRow(true) || this.checkRow(false) || this.checkCol(true) || this.checkCol(false);
1298
        }
1299
 
1300
        public final Range getCurrentRegion() {
1301
            while (this.checkFrame())
1302
                ;// bounded by table size
25 ilm 1303
            return new Range(getName(), new Point(this.minX, this.minY), new Point(this.maxX, this.maxY));
17 ilm 1304
        }
1305
    }
1306
 
1307
    public final Range getCurrentRegion(String ref) {
1308
        return this.getCurrentRegion(ref, false);
1309
    }
1310
 
1311
    public final Range getCurrentRegion(String ref, boolean checkStyle) {
1312
        final Point p = resolveHint(ref);
1313
        return this.getCurrentRegion(p.x, p.y, checkStyle);
1314
    }
1315
 
1316
    /**
1317
     * The smallest range containing the passed cell completely surrounded by empty rows and
1318
     * columns.
1319
     *
1320
     * @param startX x coordinate.
1321
     * @param startY y coordinate.
1322
     * @return the smallest range containing the passed cell.
25 ilm 1323
     * @see <a href="http://msdn.microsoft.com/library/aa214248(v=office.11).aspx">CurrentRegion
1324
     *      Property</a>
17 ilm 1325
     */
1326
    public final Range getCurrentRegion(final int startX, final int startY) {
1327
        return this.getCurrentRegion(startX, startY, false);
1328
    }
1329
 
1330
    public final Range getCurrentRegion(final int startX, final int startY, final boolean checkStyle) {
1331
        return new RegionExplorer(startX, startY, checkStyle).getCurrentRegion();
1332
    }
1333
 
1334
    // *** static
1335
 
1336
    /**
1337
     * Convert string coordinates into numeric ones.
1338
     *
1339
     * @param ref the string address, eg "$AA$34" or "AA34".
1340
     * @return the numeric coordinates or <code>null</code> if <code>ref</code> is not valid, eg
1341
     *         {26, 33}.
1342
     */
1343
    static final Point resolve(String ref) {
1344
        final Matcher matcher = SpreadSheet.minCellPattern.matcher(ref);
1345
        if (!matcher.matches())
1346
            return null;
1347
        return resolve(matcher.group(1), matcher.group(2));
1348
    }
1349
 
1350
    /**
1351
     * Convert string coordinates into numeric ones. ATTN this method does no checks.
1352
     *
1353
     * @param letters the column, eg "AA".
1354
     * @param digits the row, eg "34".
1355
     * @return the numeric coordinates, eg {26, 33}.
1356
     */
1357
    static final Point resolve(final String letters, final String digits) {
1358
        return new Point(toInt(letters), Integer.parseInt(digits) - 1);
1359
    }
1360
 
1361
    // "AA" => 26
1362
    static final int toInt(String col) {
1363
        if (col.length() < 1)
1364
            throw new IllegalArgumentException("x cannot be empty");
1365
        col = col.toUpperCase();
1366
 
1367
        int x = 0;
1368
        for (int i = 0; i < col.length(); i++) {
1369
            x = x * 26 + (col.charAt(i) - 'A' + 1);
1370
        }
1371
 
1372
        // zero based
1373
        return x - 1;
1374
    }
1375
 
41 ilm 1376
    public static final String toStr(int col) {
17 ilm 1377
        if (col < 0)
1378
            throw new IllegalArgumentException("negative column : " + col);
1379
        // one based (i.e. 0 is A)
1380
        col++;
1381
 
1382
        final int radix = 26;
1383
        final StringBuilder chars = new StringBuilder(4);
1384
        while (col > 0) {
1385
            chars.append((char) ('A' + ((col - 1) % radix)));
1386
            col = (col - 1) / radix;
1387
        }
1388
 
1389
        return chars.reverse().toString();
1390
    }
1391
 
1392
    /**
1393
     * Convert numeric coordinates into string ones.
1394
     *
1395
     * @param p the numeric coordinates, e.g. {26, 33}.
1396
     * @return the string address, e.g. "AA34".
1397
     */
1398
    static final String getAddress(Point p) {
156 ilm 1399
        return getAddress(p.x, p.y);
17 ilm 1400
    }
156 ilm 1401
 
1402
    static final String getAddress(final int x, final int y) {
1403
        if (x < 0 || y < 0)
1404
            throw new IllegalArgumentException("negative coordinates : " + x + ":" + y);
1405
        return toStr(x) + (y + 1);
1406
    }
17 ilm 1407
}