sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: Complete (for now) the implementation of GridCoverage.toString(). This tasks required improvement in GridGeometry.toString(), SampleDimension.toString(), TreeTableFormat and TableAppender.
Date Mon, 17 Dec 2018 15:50:57 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 679796f8596d30af0994c035fb4abd58d940ee5e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Dec 17 16:41:22 2018 +0100

    Complete (for now) the implementation of GridCoverage.toString(). This tasks required improvement in GridGeometry.toString(), SampleDimension.toString(), TreeTableFormat and TableAppender.
---
 .../sis/internal/jaxb/SpecializedIdentifier.java   |   2 +-
 .../org/apache/sis/internal/jaxb/gml/Measure.java  |   3 +-
 .../java/org/apache/sis/test/MetadataAssert.java   |  10 +-
 .../org/apache/sis/coverage/SampleDimension.java   |  17 +-
 .../org/apache/sis/coverage/SampleRangeFormat.java | 218 ++++++++++++++-------
 .../org/apache/sis/coverage/grid/GridChange.java   |  12 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |  49 ++++-
 .../org/apache/sis/coverage/grid/GridExtent.java   |  16 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 174 +++++++++-------
 .../org/apache/sis/internal/raster/Resources.java  |   5 +
 .../sis/internal/raster/Resources.properties       |   1 +
 .../sis/internal/raster/Resources_fr.properties    |   1 +
 .../apache/sis/coverage/grid/GridExtentTest.java   |  31 ++-
 .../org/apache/sis/parameter/ParameterFormat.java  |   5 +-
 .../operation/builder/LinearTransformBuilder.java  |   3 +-
 .../referencing/operation/matrix/MatrixSIS.java    |   2 +-
 .../projection/MercatorMethodComparison.java       |   5 +-
 .../operation/transform/MathTransformTestCase.java |   3 +-
 .../main/java/org/apache/sis/io/TableAppender.java |  23 ++-
 .../java/org/apache/sis/measure/UnitDimension.java |   3 +-
 .../java/org/apache/sis/measure/UnitFormat.java    |   5 +-
 .../sis/util/collection/TreeTableFormat.java       |  35 +++-
 .../apache/sis/util/logging/MonolineFormatter.java |   3 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  10 +
 .../sis/util/resources/Vocabulary.properties       |   2 +
 .../sis/util/resources/Vocabulary_fr.properties    |   2 +
 .../test/java/org/apache/sis/test/TestSuite.java   |   7 +-
 .../org/apache/sis/util/collection/CacheTest.java  |   3 +-
 .../sis/util/collection/TreeTableFormatTest.java   |  33 +++-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java |   3 +-
 .../apache/sis/internal/storage/gpx/Metadata.java  |   3 +-
 .../sis/internal/storage/gpx/WriterTest.java       |   3 +-
 32 files changed, 489 insertions(+), 203 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
index 2b89294..8adcaa7 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
@@ -256,7 +256,7 @@ public final class SpecializedIdentifier<T> implements Identifier, Cloneable, Se
         try {
             return super.clone();
         } catch (CloneNotSupportedException e) {
-            throw new AssertionError(e);    // Should never happen, since we are cloneable.
+            throw new AssertionError(e);            // Should never happen, since we are cloneable.
         }
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gml/Measure.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gml/Measure.java
index 62dc36b..c01cc0d 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gml/Measure.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gml/Measure.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.jaxb.gml;
 
 import java.util.Locale;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URISyntaxException;
 import javax.measure.Unit;
 import javax.measure.Quantity;
@@ -183,7 +184,7 @@ public final class Measure {
         try {
             UCUM.format(unit, link);
         } catch (IOException e) {
-            throw new AssertionError(e);        // Should never happen since we wrote to a StringBuilder.
+            throw new UncheckedIOException(e);          // Should never happen since we wrote to a StringBuilder.
         }
         return link.append("'])").toString();
     }
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
index 8a045fd..e73e564 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
@@ -236,10 +236,12 @@ public strictfp class MetadataAssert extends Assert {
         try {
             comparator = new DocumentComparator(expected, actual);
         } catch (IOException | ParserConfigurationException | SAXException e) {
-            // We don't throw directly those exceptions since failing to parse the XML file can
-            // be considered as part of test failures and the JUnit exception for such failures
-            // is AssertionError. Having no checked exception in "assert" methods allow us to
-            // declare the checked exceptions only for the library code being tested.
+            /*
+             * We don't throw directly those exceptions since failing to parse the XML file can
+             * be considered as part of test failures and the JUnit exception for such failures
+             * is AssertionError. Having no checked exception in "assert" methods allow us to
+             * declare the checked exceptions only for the library code being tested.
+             */
             throw new AssertionError(e);
         }
         comparator.tolerance = tolerance;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 0e77f72..fce8b7b 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -38,6 +38,7 @@ import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -451,7 +452,21 @@ public class SampleDimension implements Serializable {
      */
     @Override
     public String toString() {
-        return new SampleRangeFormat(Locale.getDefault()).format(name, categories);
+        return new SampleRangeFormat(Locale.getDefault()).write(new SampleDimension[] {this});
+    }
+
+    /**
+     * Returns a string representation of the given sample dimensions.
+     * This string is for debugging purpose only and may change in future version.
+     *
+     * @param  locale      the locale to use for formatting texts.
+     * @param  dimensions  the sample dimensions to format.
+     * @return a string representation of the given sample dimensions for debugging purpose.
+     */
+    @Debug
+    public static String toString(final Locale locale, SampleDimension... dimensions) {
+        ArgumentChecks.ensureNonNull("dimensions", dimensions);
+        return new SampleRangeFormat(locale).write(dimensions);
     }
 
 
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
index 8999af1..f7c62d6 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
@@ -16,10 +16,10 @@
  */
 package org.apache.sis.coverage;
 
-import java.util.List;
 import java.util.Locale;
 import java.text.NumberFormat;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.text.DecimalFormat;
 import org.opengis.util.GenericName;
 import org.apache.sis.io.TableAppender;
@@ -42,15 +42,15 @@ import org.apache.sis.util.resources.Vocabulary;
 @SuppressWarnings({"CloneableClassWithoutClone", "serial"})         // Not intended to be cloned or serialized.
 final class SampleRangeFormat extends RangeFormat {
     /**
-     * Maximum value for {@link #ndigits}. This is the number of
-     * significant digits to allow when formatting real values.
+     * Maximum value for {@link #numFractionDigits}. This is the number
+     * of significant digits to allow when formatting real values.
      */
     private static final int MAX_DIGITS = 6;
 
     /**
      * Number of significant digits used for formatting real values.
      */
-    private int ndigits;
+    private int[] numFractionDigits;
 
     /**
      * {@code true} if the range of sample values is different than the range of real values, or
@@ -59,7 +59,7 @@ final class SampleRangeFormat extends RangeFormat {
     private boolean hasPackedValues;
 
     /**
-     * Whether {@link #prepare(List)} found at least one quantitative category.
+     * Whether {@link #prepare(SampleDimension[])} found at least one quantitative category.
      * If {@code false}, then we can omit the "Measures" column.
      */
     private boolean hasQuantitative;
@@ -70,6 +70,11 @@ final class SampleRangeFormat extends RangeFormat {
     private final Vocabulary words;
 
     /**
+     * Index of the current sample dimension being formatted.
+     */
+    private int currentIndex;
+
+    /**
      * Creates a new format for the given locale.
      *
      * @param locale   the locale for table header, category names and number format.
@@ -84,31 +89,36 @@ final class SampleRangeFormat extends RangeFormat {
      * This method assumes that real values in the range {@code Category.converse.range} are stored
      * as integer sample values in the range {@code Category.range}.
      */
-    private void prepare(final List<Category> categories) {
-        ndigits         = 0;
-        hasPackedValues = false;
-        hasQuantitative = false;
-        for (final Category category : categories) {
-            final Category converted = category.converted();
-            final boolean  isPacked  = (category.minimum != converted.minimum)
-                                     | (category.maximum != converted.maximum);
-            hasPackedValues |= isPacked;
-            /*
-             * If the sample values are already real values, pretend that they are packed in bytes.
-             * The intent is only to compute an arbitrary number of fraction digits.
-             */
-            final double range = isPacked ? ( category.maximum -  category.minimum) : 255;
-            final double increment =        (converted.maximum - converted.minimum) / range;
-            if (!Double.isNaN(increment)) {
-                hasQuantitative = true;
-                final int n = -Numerics.toExp10(Math.getExponent(increment));
-                if (n > ndigits) {
-                    ndigits = n;
+    private void prepare(final SampleDimension[] dimensions) {
+        final int count   = dimensions.length;
+        numFractionDigits = new int[count];
+        hasPackedValues   = false;
+        hasQuantitative   = false;
+        for (int i=0; i<count; i++) {
+            int ndigits = 0;
+            for (final Category category : dimensions[i].getCategories()) {
+                final Category converted = category.converted();
+                final boolean  isPacked  = (category.minimum != converted.minimum)
+                                         | (category.maximum != converted.maximum);
+                hasPackedValues |= isPacked;
+                /*
+                 * If the sample values are already real values, pretend that they are packed in bytes.
+                 * The intent is only to compute an arbitrary number of fraction digits.
+                 */
+                final double range = isPacked ? ( category.maximum -  category.minimum) : 255;
+                final double increment =        (converted.maximum - converted.minimum) / range;
+                if (!Double.isNaN(increment)) {
+                    hasQuantitative = true;
+                    final int n = -Numerics.toExp10(Math.getExponent(increment));
+                    if (n > ndigits) {
+                        ndigits = n;
+                    }
                 }
             }
-        }
-        if (ndigits >= MAX_DIGITS) {
-            ndigits = MAX_DIGITS;
+            if (ndigits >= MAX_DIGITS) {
+                ndigits = MAX_DIGITS;
+            }
+            numFractionDigits[i] = ndigits;
         }
     }
 
@@ -127,7 +137,7 @@ final class SampleRangeFormat extends RangeFormat {
         if (value instanceof Number) {
             final double m = Math.abs(((Number) value).doubleValue());
             final String text;
-            if ((m >= 1E+9 || m < 1E-4) && elementFormat instanceof DecimalFormat) {
+            if (m > 0 && (m >= 1E+9 || m < 1E-4) && elementFormat instanceof DecimalFormat) {
                 final DecimalFormat df = (DecimalFormat) elementFormat;
                 final String pattern = df.toPattern();
                 df.applyPattern("0.######E00");
@@ -155,7 +165,7 @@ final class SampleRangeFormat extends RangeFormat {
     /**
      * Formats a range of measurements. There is usually only zero or one range of measurement per {@link SampleDimension},
      * but {@code SampleRangeFormat} is not restricted to that limit. The number of fraction digits to use should have been
-     * computed by {@link #prepare(List)} before to call this method.
+     * computed by {@link #prepare(SampleDimension[])} before to call this method.
      *
      * @return the range to write, or {@code null} if the given {@code range} argument was null.
      */
@@ -166,6 +176,7 @@ final class SampleRangeFormat extends RangeFormat {
         final NumberFormat nf = (NumberFormat) elementFormat;
         final int min = nf.getMinimumFractionDigits();
         final int max = nf.getMaximumFractionDigits();
+        final int ndigits = numFractionDigits[currentIndex];
         nf.setMinimumFractionDigits(ndigits);
         nf.setMaximumFractionDigits(ndigits);
         final String text = format(range);
@@ -175,29 +186,15 @@ final class SampleRangeFormat extends RangeFormat {
     }
 
     /**
-     * Returns a string representation of the given list of categories.
-     *
-     * @param title       caption for the table.
-     * @param categories  the list of categories to format.
-     */
-    final String format(final GenericName title, final CategoryList categories) {
-        final StringBuilder buffer = new StringBuilder(800);
-        try {
-            format(title, categories, buffer);
-        } catch (IOException e) {
-            throw new AssertionError(e);    // Should never happen since we write to a StringBuilder.
-        }
-        return buffer.toString();
-    }
-
-    /**
      * Formats a string representation of the given list of categories.
      * This method formats a table like below:
      *
      * {@preformat text
      *   ┌────────────┬────────────────┬─────────────┐
      *   │   Values   │    Measures    │    Name     │
-     *   ├────────────┼────────────────┼─────────────┤
+     *   ╞════════════╧════════════════╧═════════════╡
+     *   │Band 1                                     │
+     *   ├────────────┬────────────────┬─────────────┤
      *   │         0  │ NaN #0         │ No data     │
      *   │         1  │ NaN #1         │ Clouds      │
      *   │         5  │ NaN #5         │ Lands       │
@@ -205,50 +202,119 @@ final class SampleRangeFormat extends RangeFormat {
      *   └────────────┴────────────────┴─────────────┘
      * }
      *
-     * @param title       caption for the table.
-     * @param categories  the list of categories to format.
-     * @param out         where to write the category table.
+     * @param dimensions  the list of sample dimensions to format.
      */
-    void format(final GenericName title, final CategoryList categories, final Appendable out) throws IOException {
-        prepare(categories);
-        final String lineSeparator = System.lineSeparator();
-        out.append(title.toInternationalString().toString(getLocale())).append(lineSeparator);
+    String write(final SampleDimension[] dimensions) {
+        prepare(dimensions);
         /*
          * Write table header: │ Values │ Measures │ name │
          */
-        final TableAppender table = new TableAppender(out, " │ ");
+        final StringBuilder buffer = new StringBuilder(800);
+        final TableAppender table = new TableAppender(buffer, " │ ");
+        table.setMultiLinesCells(true);
         table.appendHorizontalSeparator();
         table.setCellAlignment(TableAppender.ALIGN_CENTER);
         if (hasPackedValues) table.append(words.getString(Vocabulary.Keys.Values))  .nextColumn();
         if (hasQuantitative) table.append(words.getString(Vocabulary.Keys.Measures)).nextColumn();
         /* Unconditional  */ table.append(words.getString(Vocabulary.Keys.Name))    .nextLine();
+        table.nextLine('═');
+        table.append('#');                      // Dummy character to be replaced by band name later.
+        table.appendHorizontalSeparator();
+        for (final SampleDimension dim : dimensions) {
+            for (final Category category : dim.getCategories()) {
+                /*
+                 * "Sample values" column. Omitted if all values are already real values.
+                 */
+                if (hasPackedValues) {
+                    table.setCellAlignment(TableAppender.ALIGN_RIGHT);
+                    table.append(formatSample(category.getRangeLabel()));
+                    table.nextColumn();
+                }
+                table.setCellAlignment(TableAppender.ALIGN_LEFT);
+                /*
+                 * "Real values" column. Omitted if no category has a transfer function.
+                 */
+                if (hasQuantitative) {
+                    final Category converted = category.converted();
+                    String text = formatMeasure(converted.range);               // Example: [6.0 … 25.0)°C
+                    if (text == null) {
+                        text = String.valueOf(converted.getRangeLabel());       // Example: NaN #0
+                    }
+                    table.append(text);
+                    table.nextColumn();
+                }
+                table.append(category.name.toString(getLocale()));
+                table.nextLine();
+            }
+        }
         table.appendHorizontalSeparator();
-        for (final Category category : categories) {
+        try {
+            table.flush();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);      // Should never happen since we write to a StringBuilder.
+        }
+        /*
+         * After we formatted the table, insert the sample dimension names before each category lists.
+         * We do that after formatting table because TableAppender currently has no API for spanning
+         * a value on many cells. The following code changes some characters but do not change buffer
+         * length.
+         */
+        int lastDimensionEnd = 0;
+        final String lineSeparator = table.getLineSeparator();
+        final String toSearch = lineSeparator + '╞';
+        for (final SampleDimension dim : dimensions) {
+            int lineStart = buffer.indexOf(toSearch, lastDimensionEnd);
+            if (lineStart < 0) break;                                           // Should not happen.
+            lineStart += toSearch.length();
+            int i = replace(buffer, lineStart, '╪', '╧', '╡');
+            int limit = (i-2) - lineStart;                                      // Space available in a row.
+            i += lineSeparator.length() + 2;                                    // Beginning of next line.
             /*
-             * "Sample values" column. Omitted if all values are already real values.
+             * At this point, 'i' is at the beginning of the row where to format the band name.
+             * The line above that row has been modified for removing vertical lines. Now fill
+             * the space in current row with band name and pad with white spaces.
              */
-            if (hasPackedValues) {
-                table.setCellAlignment(TableAppender.ALIGN_RIGHT);
-                table.append(formatSample(category.getRangeLabel()));
-                table.nextColumn();
+            final GenericName name = dim.getName();
+            String label;
+            if (name != null) {
+                label = name.toInternationalString().toString(getLocale());
+            } else {
+                label = words.getString(Vocabulary.Keys.Unnamed);
             }
-            table.setCellAlignment(TableAppender.ALIGN_LEFT);
+            if (label.length() > limit) {
+                label = label.substring(0, limit);
+            }
+            limit += i;                                         // Now an absolute index instead than a length.
+            buffer.replace(i, i += label.length(), label);
+            while (i < limit) buffer.setCharAt(i++, ' ');
             /*
-             * "Real values" column. Omitted if no category has a transfer function.
+             * At this point the sample dimension name has been written.
+             * Update the next line and move to the next sample dimension.
              */
-            if (hasQuantitative) {
-                final Category converted = category.converted();
-                String text = formatMeasure(converted.range);               // Example: [6.0 … 25.0)°C
-                if (text == null) {
-                    text = String.valueOf(converted.getRangeLabel());       // Example: NaN #0
-                }
-                table.append(text);
-                table.nextColumn();
-            }
-            table.append(category.name.toString(getLocale()));
-            table.nextLine();
+            lastDimensionEnd = replace(buffer, i + lineSeparator.length() + 2, '┼', '┬', '┤');
         }
-        table.appendHorizontalSeparator();
-        table.flush();
+        return buffer.toString();
+    }
+
+    /**
+     * Replaces characters in the given buffer until a sentinel value, which must exist.
+     *
+     * @param  buffer   the buffer where to perform the replacements.
+     * @param  i        index of the first character to check.
+     * @param  search   character to search for replacement.
+     * @param  replace  character to use as a replacement.
+     * @param  stop     sentinel value for stopping the search.
+     * @return index after the sentinel value.
+     */
+    private static int replace(final StringBuilder buffer, int i, final char search, final char replace, final char stop) {
+        char c;
+        do {
+            c = buffer.charAt(i);
+            if (c == search) {
+                buffer.setCharAt(i, replace);
+            }
+            i++;
+        } while (c != stop);
+        return i;
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
index d61aab8..2079205 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
@@ -28,9 +28,10 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.geometry.Envelopes;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Classes;
 
 
 /**
@@ -307,17 +308,18 @@ public class GridChange implements Serializable {
 
     /**
      * Returns a string representation of this grid change for debugging purpose.
+     * The returned string is implementation dependent and may change in any future version.
      *
-     * @return a string representation for debugging purpose.
+     * @return a string representation of this grid change for debugging purpose.
      */
     @Override
     public String toString() {
         final String lineSeparator = System.lineSeparator();
         final StringBuilder buffer = new StringBuilder(256)
-                .append("Grid change").append(lineSeparator)
+                .append(Classes.getShortClassName(this)).append(lineSeparator)
                 .append("└ Scale factor ≈ ").append((float) getGlobalScale()).append(lineSeparator)
                 .append("Target range").append(lineSeparator);
-        targetRange.appendTo(buffer, Vocabulary.getResources((Locale) null), true);
+        targetRange.appendTo(buffer, Vocabulary.getResources((Locale) null));
         return buffer.toString();
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index 64eaa1c..aebef90 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -18,6 +18,7 @@ package org.apache.sis.coverage.grid;
 
 import java.util.List;
 import java.util.Collection;
+import java.util.Locale;
 import java.awt.image.RenderedImage;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.coverage.CannotEvaluateException;
@@ -25,7 +26,13 @@ import org.opengis.coverage.PointOutsideCoverageException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -136,15 +143,47 @@ public abstract class GridCoverage {
 
     /**
      * Returns a string representation of this grid coverage for debugging purpose.
+     * The returned string is implementation dependent and may change in any future version.
+     * Current implementation is equivalent to the following, where {@code EXTENT}, <i>etc.</i> are
+     * constants defined in {@link GridGeometry} class:
+     *
+     * {@preformat java
+     *   return toTree(Locale.getDefault(), EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION).toString();
+     * }
      *
      * @return a string representation of this grid coverage for debugging purpose.
      */
     @Override
     public String toString() {
-        final String lineSeparator = System.lineSeparator();
-        final StringBuilder buffer = new StringBuilder(1000);
-        buffer.append("Grid coverage domain:").append(lineSeparator);
-        gridGeometry.formatTo(buffer, lineSeparator + "  ");
-        return buffer.toString();
+        return toTree(Locale.getDefault(), GridGeometry.EXTENT | GridGeometry.ENVELOPE
+                | GridGeometry.CRS | GridGeometry.GRID_TO_CRS | GridGeometry.RESOLUTION).toString();
+    }
+
+    /**
+     * Returns a tree representation of some elements of this grid coverage.
+     * The tree representation is for debugging purpose only and may change
+     * in any future SIS version.
+     *
+     * @param  locale   the locale to use for textual labels.
+     * @param  bitmask  combination of {@link GridGeometry} flags.
+     * @return a tree representation of the specified elements.
+     *
+     * @see GridGeometry#toTree(Locale, int)
+     */
+    @Debug
+    public TreeTable toTree(final Locale locale, final int bitmask) {
+        ArgumentChecks.ensureNonNull("locale", locale);
+        final Vocabulary vocabulary = Vocabulary.getResources(locale);
+        final TableColumn<CharSequence> column = TableColumn.VALUE_AS_TEXT;
+        final TreeTable tree = new DefaultTreeTable(column);
+        final TreeTable.Node root = tree.getRoot();
+        root.setValue(column, Classes.getShortClassName(this));
+        TreeTable.Node branch = root.newChild();
+        branch.setValue(column, vocabulary.getString(Vocabulary.Keys.CoverageDomain));
+        gridGeometry.formatTo(locale, vocabulary, bitmask, branch);
+        branch = root.newChild();
+        branch.setValue(column, vocabulary.getString(Vocabulary.Keys.SampleDimensions));
+        branch.newChild().setValue(column, SampleDimension.toString(locale, sampleDimensions));
+        return tree;
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 0f8ea50..72865e6 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -727,18 +727,18 @@ public class GridExtent implements Serializable {
     @Override
     public String toString() {
         final StringBuilder out = new StringBuilder(256);
-        appendTo(out, Vocabulary.getResources((Locale) null), false);
+        appendTo(out, Vocabulary.getResources((Locale) null));
         return out.toString();
     }
 
     /**
      * Writes a string representation of this grid envelope in the given buffer.
+     * This method is provided for allowing caller to recycle the same buffer.
      *
      * @param out         where to write the string representation.
      * @param vocabulary  resources for some words.
-     * @param tree        whether to format lines of a tree in the margin on the left.
      */
-    final void appendTo(final StringBuilder out, final Vocabulary vocabulary, final boolean tree) {
+    final void appendTo(final StringBuilder out, final Vocabulary vocabulary) {
         final TableAppender table = new TableAppender(out, "");
         final int dimension = getDimension();
         for (int i=0; i<dimension; i++) {
@@ -749,9 +749,6 @@ public class GridExtent implements Serializable {
             final long lower = coordinates[i];
             final long upper = coordinates[i + dimension];
             table.setCellAlignment(TableAppender.ALIGN_LEFT);
-            if (tree) {
-                branch(table, i < dimension - 1);
-            }
             table.append(name).append(": ").nextColumn();
             table.append('[').nextColumn();
             table.setCellAlignment(TableAppender.ALIGN_RIGHT);
@@ -764,13 +761,6 @@ public class GridExtent implements Serializable {
     }
 
     /**
-     * Formats the symbols on the left side of a node in a tree.
-     */
-    static void branch(final TableAppender table, final boolean hasMore) {
-        table.append(hasMore ? '├' : '└').append("─ ");
-    }
-
-    /**
      * Writes the content of given table without throwing {@link IOException}.
      * Shall be invoked only when the destination is known to be {@link StringBuilder}.
      */
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 76b89f1..8a45473 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -46,11 +46,15 @@ import org.apache.sis.referencing.operation.transform.PassThroughTransform;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.raster.Resources;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 import org.apache.sis.io.TableAppender;
 
@@ -917,43 +921,47 @@ public class GridGeometry implements Serializable {
     }
 
     /**
-     * Returns a string representation of this grid geometry. The returned string
-     * is implementation dependent. It is provided for debugging purposes only.
+     * Returns a string representation of this grid geometry.
+     * The returned string is implementation dependent and may change in any future version.
      * Current implementation is equivalent to the following:
      *
      * {@preformat java
-     *   return toString(EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION);
+     *   return toTree(Locale.getDefault(), EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION).toString();
      * }
      */
     @Override
     public String toString() {
-        return toString(EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION);
+        return toTree(Locale.getDefault(), EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION).toString();
     }
 
     /**
-     * Returns a string representation of some elements of this grid geometry.
-     * The string representation is for debugging purpose only and may change
+     * Returns a tree representation of some elements of this grid geometry.
+     * The tree representation is for debugging purpose only and may change
      * in any future SIS version.
      *
+     * @param  locale   the locale to use for textual labels.
      * @param  bitmask  combination of {@link #EXTENT}, {@link #ENVELOPE},
      *         {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
-     * @return a string representation of the given elements.
+     * @return a tree representation of the specified elements.
      */
     @Debug
-    public String toString(final int bitmask) {
-        if ((bitmask & ~(EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION)) != 0) {
-            throw new IllegalArgumentException(Errors.format(
-                    Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
-        }
-        return new Formatter(new StringBuilder(512), bitmask, System.lineSeparator()).toString();
+    public TreeTable toTree(final Locale locale, final int bitmask) {
+        final TreeTable tree = new DefaultTreeTable(TableColumn.VALUE_AS_TEXT);
+        final TreeTable.Node root = tree.getRoot();
+        root.setValue(TableColumn.VALUE_AS_TEXT, Classes.getShortClassName(this));
+        formatTo(locale, Vocabulary.getResources(locale), bitmask, root);
+        return tree;
     }
 
     /**
-     * Formats a string representation of this grid geometry in the specified buffer.
-     * This is used for reducing the amount of copies in {@link GridCoverage#toString()}.
+     * Formats a string representation of this grid geometry in the specified tree.
      */
-    final void formatTo(final StringBuilder out, final String lineSeparator) {
-        new Formatter(out, EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION, lineSeparator).format();
+    final void formatTo(final Locale locale, final Vocabulary vocabulary, final int bitmask, final TreeTable.Node root) {
+        if ((bitmask & ~(EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION)) != 0) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
+        }
+        new Formatter(locale, vocabulary, bitmask, root).format();
     }
 
     /**
@@ -967,14 +975,20 @@ public class GridGeometry implements Serializable {
         private final int bitmask;
 
         /**
-         * Where to write the {@link GridGeometry} string representation.
+         * Temporary buffer for formatting node values.
          */
         private final StringBuilder buffer;
 
         /**
-         * Platform-specific end-of-line characters.
+         * Where to write the {@link GridGeometry} string representation.
+         */
+        private final TreeTable.Node root;
+
+        /**
+         * The section under the {@linkplain #root} where to write elements.
+         * This is updated when {@link #section(int, short, Object, boolean)} is invoked.
          */
-        private final String lineSeparator;
+        private TreeTable.Node section;
 
         /**
          * Localized words.
@@ -982,7 +996,7 @@ public class GridGeometry implements Serializable {
         private final Vocabulary vocabulary;
 
         /**
-         * The locale for the texts. Not used for numbers and dates.
+         * The locale for the texts. Not used for numbers.
          */
         private final Locale locale;
 
@@ -1000,26 +1014,17 @@ public class GridGeometry implements Serializable {
          * Creates a new formatter for the given combination of {@link #EXTENT}, {@link #ENVELOPE},
          * {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
          */
-        Formatter(final StringBuilder out, final int bitmask, final String lineSeparator) {
-            this.buffer        = out;
+        Formatter(final Locale locale, final Vocabulary vocabulary, final int bitmask, final TreeTable.Node out) {
+            this.root          = out;
             this.bitmask       = bitmask;
-            this.lineSeparator = lineSeparator;
-            this.locale        = Locale.getDefault(Locale.Category.DISPLAY);
-            this.vocabulary    = Vocabulary.getResources(locale);
+            this.buffer        = new StringBuilder(256);
+            this.locale        = locale;
+            this.vocabulary    = vocabulary;
             this.crs           = (envelope != null) ? envelope.getCoordinateReferenceSystem() : null;
             this.cs            = (crs != null) ? crs.getCoordinateSystem() : null;
         }
 
         /**
-         * Returns a string representation of the enclosing {@link GridGeometry} instance.
-         */
-        @Override
-        public final String toString() {
-            format();
-            return buffer.toString();
-        }
-
-        /**
          * Formats a string representation of the enclosing {@link GridGeometry} instance
          * in the buffer specified at construction time.
          */
@@ -1029,15 +1034,16 @@ public class GridGeometry implements Serializable {
              * ├─ Dimension 0: [370 … 389]  (20 cells)
              * └─ Dimension 1: [ 41 … 340] (300 cells)
              */
-            if (section(EXTENT, Vocabulary.Keys.GridExtent, extent)) {
-                extent.appendTo(buffer, vocabulary, true);
+            if (section(EXTENT, Vocabulary.Keys.GridExtent, extent, false)) {
+                extent.appendTo(buffer, vocabulary);
+                writeNodes();
             }
             /*
              * Example: Envelope
              * ├─ Geodetic latitude:  -69.75 … 80.25  Δφ = 0.5°
              * └─ Geodetic longitude:   4.75 … 14.75  Δλ = 0.5°
              */
-            if (section(ENVELOPE, Vocabulary.Keys.Envelope, envelope)) {
+            if (section(ENVELOPE, Vocabulary.Keys.Envelope, envelope, false)) {
                 final boolean appendResolution = (bitmask & RESOLUTION) != 0 && resolution != null;
                 final TableAppender table = new TableAppender(buffer, "");
                 final int dimension = envelope.getDimension();
@@ -1045,7 +1051,6 @@ public class GridGeometry implements Serializable {
                     final CoordinateSystemAxis axis = (cs != null) ? cs.getAxis(i) : null;
                     final String name = (axis != null) ? axis.getName().getCode() :
                             vocabulary.getString(Vocabulary.Keys.Dimension_1, i);
-                    GridExtent.branch(table, i < dimension - 1);
                     table.append(name).append(": ").nextColumn();
                     table.setCellAlignment(TableAppender.ALIGN_RIGHT);
                     table.append(Double.toString(envelope.getLower(i))).nextColumn();
@@ -1065,54 +1070,53 @@ public class GridGeometry implements Serializable {
                     table.nextLine();
                 }
                 GridExtent.flush(table);
-            } else if (section(RESOLUTION, Vocabulary.Keys.Resolution, resolution)) {
+                writeNodes();
+            } else if (section(RESOLUTION, Vocabulary.Keys.Resolution, resolution, false)) {
                 /*
                  * Example: Resolution
                  * └─ 0.5° × 0.5°
                  */
-                String separator = "└─ ";
+                String separator = "";
                 for (int i=0; i<resolution.length; i++) {
                     appendResolution(buffer.append(separator), i);
                     separator = " × ";
                 }
-                buffer.append(lineSeparator);
+                writeNode();
             }
             /*
              * Example: Coordinate reference system
              * └─ EPSG:4326 — WGS 84 (φ,λ)
              */
-            if (section(CRS, Vocabulary.Keys.CoordinateRefSys, crs)) {
-                buffer.append("└─ ");
+            if (section(CRS, Vocabulary.Keys.CoordinateRefSys, crs, false)) {
                 final Identifier id = IdentifiedObjects.getIdentifier(crs, null);
                 if (id != null) {
                     buffer.append(IdentifiedObjects.toString(id)).append(" — ");
                 }
-                buffer.append(crs.getName()).append(lineSeparator);
+                buffer.append(crs.getName());
+                writeNode();
             }
             /*
              * Example: Conversion
              * └─ 2D → 2D non linear in 2
              */
-            if (section(GRID_TO_CRS, Vocabulary.Keys.Conversion, gridToCRS)) {
-                final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
+            final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
+            if (section(GRID_TO_CRS, Vocabulary.Keys.Conversion, gridToCRS, matrix != null)) {
                 if (matrix != null) {
-                    String separator = "└─ ";
-                    for (final CharSequence line : CharSequences.splitOnEOL(Matrices.toString(matrix))) {
-                        buffer.append(separator).append(line).append(lineSeparator);
-                        separator = "   ";
-                    }
+                    writeNode(Matrices.toString(matrix));
                 } else {
-                    buffer.append("└─ ").append(gridToCRS.getSourceDimensions()).append("D → ")
-                                        .append(gridToCRS.getTargetDimensions()).append('D');
+                    buffer.append(gridToCRS.getSourceDimensions()).append("D → ")
+                          .append(gridToCRS.getTargetDimensions()).append("D ");
                     long nonLinearDimensions = nonLinears;
-                    String separator = " non linear in ";
+                    String separator = Resources.forLocale(locale)
+                            .getString(Resources.Keys.NonLinearInDimensions_1, Long.bitCount(nonLinearDimensions));
                     while (nonLinearDimensions != 0) {
                         final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
                         nonLinearDimensions &= ~(1L << i);
-                        buffer.append(separator).append(cs != null ? cs.getAxis(i).getName().getCode() : String.valueOf(i));
-                        separator = ", ";
+                        buffer.append(separator).append(' ')
+                              .append(cs != null ? cs.getAxis(i).getName().getCode() : String.valueOf(i));
+                        separator = ",";
                     }
-                    buffer.append(lineSeparator);
+                    writeNode();
                 }
             }
         }
@@ -1120,26 +1124,62 @@ public class GridGeometry implements Serializable {
         /**
          * Starts a new section for the given property.
          *
-         * @param  property  one of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
-         * @param  title     the {@link Vocabulary} key for the title to show for this section, if formatted.
-         * @param  value     the value to be formatted in that section.
+         * @param  property    one of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
+         * @param  title       the {@link Vocabulary} key for the title to show for this section, if formatted.
+         * @param  cellCenter  whether to add a "origin in cell center" text in the title. This is relevant only for conversion.
+         * @param  value       the value to be formatted in that section.
          * @return {@code true} if the caller shall format the value.
          */
-        private boolean section(final int property, final short title, final Object value) {
+        private boolean section(final int property, final short title, final Object value, final boolean cellCenter) {
             if ((bitmask & property) != 0) {
-                buffer.append(vocabulary.getString(title));
-                if (title == Vocabulary.Keys.Conversion) {
-                    buffer.append(" (").append(vocabulary.getString(Vocabulary.Keys.OriginInCellCenter).toLowerCase(locale)).append(')');
+                CharSequence text = vocabulary.getString(title);
+                if (cellCenter) {
+                    text = buffer.append(text).append(" (")
+                                 .append(vocabulary.getString(Vocabulary.Keys.OriginInCellCenter).toLowerCase(locale))
+                                 .append(')').toString();
+                    buffer.setLength(0);
                 }
-                buffer.append(lineSeparator);
+                section = root.newChild();
+                section.setValue(TableColumn.VALUE_AS_TEXT, text);
                 if (value != null) {
                     return true;
                 }
-                buffer.append("└─ ").append(vocabulary.getString(Vocabulary.Keys.Unspecified)).append(lineSeparator);
+                writeNode(vocabulary.getString(Vocabulary.Keys.Unspecified));
             }
             return false;
         }
 
+        /**
+         * Appends a single line as a node in the current section.
+         */
+        private void writeNode(final CharSequence line) {
+            String text = line.toString().trim();
+            if (!text.isEmpty()) {
+                section.newChild().setValue(TableColumn.VALUE_AS_TEXT, text);
+            }
+        }
+
+        /**
+         * Appends a node with current {@link #buffer} content as a single line, then clears the buffer.
+         */
+        private void writeNode() {
+            writeNode(buffer);
+            buffer.setLength(0);
+        }
+
+        /**
+         * Appends nodes with current {@link #buffer} content as multi-lines text, then clears the buffer.
+         */
+        private void writeNodes() {
+            for (final CharSequence line : CharSequences.splitOnEOL(buffer)) {
+                writeNode(line);
+            }
+            buffer.setLength(0);
+        }
+
+        /**
+         * Appends a single value on the resolution line, together with its unit of measurement.
+         */
         private void appendResolution(final Appendable out, final int dimension) {
             try {
                 out.append(Float.toString((float) resolution[dimension]));
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
index 9a7ef3f..a961474 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
@@ -134,6 +134,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short NoCategoryForValue_1 = 14;
 
         /**
+         * non-linear in {0} dimension{0,choice,1#|2#s}:
+         */
+        public static final short NonLinearInDimensions_1 = 20;
+
+        /**
          * Too many qualitative categories.
          */
         public static final short TooManyQualitatives = 17;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
index 53cdae1..a8e6193 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
@@ -34,6 +34,7 @@ MismatchedImageLocation           = The two images have different size or pixel
 MismatchedSampleModel             = The two images use different sample models.
 MismatchedTileGrid                = The two images have different tile grid.
 NoCategoryForValue_1              = No category for value {0}.
+NonLinearInDimensions_1           = non-linear in {0} dimension{0,choice,1#|2#s}:
 TooManyQualitatives               = Too many qualitative categories.
 UnspecifiedCRS                    = Coordinate reference system is unspecified.
 UnspecifiedGridExtent             = Grid extent is unspecified.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
index 29c230e..5bb7c9e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
@@ -39,6 +39,7 @@ MismatchedImageLocation           = Les deux images ont une taille ou des coordo
 MismatchedSampleModel             = Les deux images disposent les pixels diff\u00e9remment.
 MismatchedTileGrid                = Les deux images utilisent des grilles de tuiles diff\u00e9rentes.
 NoCategoryForValue_1              = Aucune cat\u00e9gorie n\u2019est d\u00e9finie pour la valeur {0}.
+NonLinearInDimensions_1           = non-lin\u00e9aire dans {0} dimension{0,choice,1#|2#s}\u2008:
 TooManyQualitatives               = Trop de cat\u00e9gories qualitatives.
 UnspecifiedCRS                    = Le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9.
 UnspecifiedGridExtent             = L\u2019\u00e9tendue de la grille n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9e.
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
index 673766d..068d993 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
@@ -16,14 +16,16 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Locale;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.apache.sis.test.Assert.*;
 
 
 /**
@@ -96,13 +98,20 @@ public final strictfp class GridExtentTest extends TestCase {
     }
 
     /**
+     * Creates a three-dimensional grid extent to be shared by different tests.
+     */
+    private static GridExtent create3D() {
+        return new GridExtent(
+                new DimensionNameType[] {DimensionNameType.COLUMN, DimensionNameType.ROW, DimensionNameType.TIME},
+                new long[] {100, 200, 40}, new long[] {500, 800, 50}, false);
+    }
+
+    /**
      * Tests {@link GridExtent#subExtent(int, int)}.
      */
     @Test
     public void testSubExtent() {
-        GridExtent extent = new GridExtent(
-                new DimensionNameType[] {DimensionNameType.COLUMN, DimensionNameType.ROW, DimensionNameType.TIME},
-                new long[] {100, 200, 40}, new long[] {500, 800, 50}, false);
+        GridExtent extent = create3D();
         extent = extent.subExtent(0, 2);
         assertEquals("dimension", 2, extent.getDimension());
         assertExtentEquals(extent, 0, 100, 499);
@@ -110,4 +119,18 @@ public final strictfp class GridExtentTest extends TestCase {
         assertEquals(DimensionNameType.COLUMN, extent.getAxisType(0).get());
         assertEquals(DimensionNameType.ROW,    extent.getAxisType(1).get());
     }
+
+    /**
+     * Tests {@link GridExtent#toString()}.
+     * Note that the string representation may change in any future SIS version.
+     */
+    @Test
+    public void testToString() {
+        final StringBuilder buffer = new StringBuilder(100);
+        create3D().appendTo(buffer, Vocabulary.getResources(Locale.ENGLISH));
+        assertMultilinesEquals(
+                "Column: [100 … 499] (400 cells)\n" +
+                "Row:    [200 … 799] (600 cells)\n" +
+                "Time:   [ 40 …  49]  (10 cells)\n", buffer);
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
index b884643..9c6698f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/ParameterFormat.java
@@ -25,13 +25,14 @@ import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.TimeZone;
+import java.io.Console;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.text.Format;
 import java.text.NumberFormat;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
 import java.text.ParseException;
-import java.io.Console;
 import java.util.concurrent.atomic.AtomicReference;
 import javax.measure.Unit;
 
@@ -1001,7 +1002,7 @@ public class ParameterFormat extends TabularFormat<Object> {
         try {
             f.format(object, out);
         } catch (IOException e) {
-            throw new AssertionError(e);    // Should never happen, since we are writing to stdout.
+            throw new UncheckedIOException(e);      // Should never happen since we are writing to stdout.
         }
         INSTANCE.set(f);
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
index e3bb860..24493c3 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.Arrays;
 import java.util.NoSuchElementException;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
@@ -1040,7 +1041,7 @@ search:         for (int j=domain(); --j >= 0;) {
             try {
                 table.flush();
             } catch (IOException e) {
-                throw new AssertionError(e);        // Should never happen since we wrote into a StringBuilder.
+                throw new UncheckedIOException(e);      // Should never happen since we wrote into a StringBuilder.
             }
         }
         return buffer.toString();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
index d2f26f4..d76a69e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/MatrixSIS.java
@@ -839,7 +839,7 @@ public abstract class MatrixSIS implements Matrix, LenientComparable, Cloneable,
         try {
             return (MatrixSIS) super.clone();
         } catch (CloneNotSupportedException e) {
-            throw new AssertionError(e); // Should never happen, since we are cloneable.
+            throw new AssertionError(e);            // Should never happen since we are cloneable.
         }
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
index 09a2681..d02ba59 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorMethodComparison.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.operation.projection;
 import java.util.Random;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.UncheckedIOException;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.math.StatisticsFormat;
@@ -252,7 +253,7 @@ public final class MercatorMethodComparison {   // No 'strictfp' keyword here si
             try {
                 format.format(stats, out);
             } catch (IOException e) {
-                throw new AssertionError(e);
+                throw new UncheckedIOException(e);
             }
             out.flush();
         }
@@ -294,7 +295,7 @@ public final class MercatorMethodComparison {   // No 'strictfp' keyword here si
         try {
             table.flush();
         } catch (IOException e) {
-            throw new AssertionError(e);
+            throw new UncheckedIOException(e);
         }
     }
 
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
index f09afcb..637c4f9 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/MathTransformTestCase.java
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.operation.transform;
 
 import java.util.Random;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import org.opengis.util.Factory;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform1D;
@@ -435,7 +436,7 @@ public abstract strictfp class MathTransformTestCase extends TransformTestCase {
         try {
             table.flush();
         } catch (IOException e) {
-            throw new AssertionError(e);
+            throw new UncheckedIOException(e);
         }
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/io/TableAppender.java b/core/sis-utility/src/main/java/org/apache/sis/io/TableAppender.java
index a51525f..aa33057 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/io/TableAppender.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/io/TableAppender.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.io.Flushable;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
@@ -68,7 +69,7 @@ import static org.apache.sis.util.Characters.isLineOrParagraphSeparator;
  * }
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see org.apache.sis.util.collection.TreeTableFormat
  *
@@ -404,6 +405,22 @@ public class TableAppender extends Appender implements Flushable {
     }
 
     /**
+     * Returns the line separator between table rows. This is the first line separator found in the
+     * text formatted as a table, or the {@linkplain System#lineSeparator() system default} if no
+     * line separator was found in the text to format.
+     *
+     * @return the line separator between table rows.
+     *
+     * @since 1.0
+     */
+    public String getLineSeparator() {
+        if (lineSeparator == null) {
+            lineSeparator = System.lineSeparator();
+        }
+        return lineSeparator;
+    }
+
+    /**
      * Returns the number of rows in this table. This count is reset to 0 by {@link #flush()}.
      *
      * @return the number of rows in this table.
@@ -502,7 +519,7 @@ public class TableAppender extends Appender implements Flushable {
              * Should never happen, because appendSurrogate(…) delegates to append(char)
              * which is overriden without 'throws IOException' clause in this class.
              */
-            throw new AssertionError(e);
+            throw new UncheckedIOException(e);
         }
         if (start != end) {
             if (skipLF && sequence.charAt(start) == '\n') {
@@ -678,7 +695,7 @@ public class TableAppender extends Appender implements Flushable {
                 writeTable();
             } catch (IOException e) {
                 // Should never happen because we are writing in a StringBuilder.
-                throw new AssertionError(e);
+                throw new UncheckedIOException(e);
             }
         }
         return super.toString();
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
index 93fc821..e0ad719 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitDimension.java
@@ -21,6 +21,7 @@ import java.util.Map;
 import java.util.LinkedHashMap;
 import java.io.Serializable;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.io.ObjectStreamException;
 import javax.measure.Dimension;
 import org.apache.sis.math.Fraction;
@@ -381,7 +382,7 @@ final class UnitDimension implements Dimension, Serializable {
         try {
             UnitFormat.formatComponents(components, UnitFormat.Style.SYMBOL, buffer);
         } catch (IOException e) {
-            throw new AssertionError(e);      // Should never happen since we are writting to a StringBuilder.
+            throw new UncheckedIOException(e);      // Should never happen since we are writting to a StringBuilder.
         }
         return buffer.toString();
     }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
index dcace05..c9c30cb 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
@@ -29,6 +29,7 @@ import java.text.ParseException;
 import java.util.ResourceBundle;
 import java.util.MissingResourceException;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.security.AccessController;
 import javax.measure.Dimension;
 import javax.measure.Unit;
@@ -895,7 +896,7 @@ appPow: if (unit == null) {
         try {
             return (StringBuffer) format((Unit<?>) unit, toAppendTo);
         } catch (IOException e) {
-            throw new AssertionError(e);      // Should never happen since we are writting to a StringBuffer.
+            throw new UncheckedIOException(e);          // Should never happen since we are writting to a StringBuffer.
         }
     }
 
@@ -911,7 +912,7 @@ appPow: if (unit == null) {
         try {
             return format(unit, new StringBuilder()).toString();
         } catch (IOException e) {
-            throw new AssertionError(e);      // Should never happen since we are writting to a StringBuilder.
+            throw new UncheckedIOException(e);      // Should never happen since we are writting to a StringBuilder.
         }
     }
 
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java
index 8578a7b..77c3ae4 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java
@@ -667,6 +667,12 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
         private boolean[] isLast;
 
         /**
+         * Whether to allows multi-lines cells instead than using Pilcrow character.
+         * This is currently supported only if the number of columns is less than 2.
+         */
+        private final boolean multiLineCells;
+
+        /**
          * The node that have already been formatted. We use this map as a safety against infinite recursivity.
          */
         private final Set<TreeTable.Node> recursivityGuard;
@@ -686,9 +692,10 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
          * @param  recursivityGuard  an initially empty set.
          */
         Writer(final Appendable out, final TreeTable tree, final TableColumn<?>[] columns,
-                final Set<TreeTable.Node> recursivityGuard)
+               final Set<TreeTable.Node> recursivityGuard)
         {
             super(columns.length >= 2 ? new TableAppender(out, "") : out);
+            multiLineCells = (super.out == out);
             this.columns = columns;
             this.formats = getFormats(columns, false);
             this.values  = new Object[columns.length];
@@ -703,7 +710,7 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
             }
             this.filter = filter;
             setTabulationExpanded(true);
-            setLineSeparator(" ¶ ");
+            setLineSeparator(multiLineCells ? TreeTableFormat.this.getLineSeparator() : " ¶ ");
         }
 
         /**
@@ -855,9 +862,18 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
          * @param  level  indentation level. The first level is 0.
          */
         final void format(final TreeTable.Node node, final int level) throws IOException {
+            /*
+             * Draw the lines of the tree in the left margin for current row.
+             */
             for (int i=0; i<level; i++) {
                 out.append(getTreeSymbols(i != level-1, isLast[i]));
             }
+            /*
+             * Fetch the values to write in current row, but do not write them now. We fetch values in advance in order
+             * to detect trailing null values, so we can avoid formatting trailing blank spaces. Note that a null value
+             * may be followed by a non-null value, which is why we need to check all of them before to know how many
+             * columns to omit.
+             */
             int n = 0;
             for (int i=0; i<columns.length; i++) {
                 if ((values[i] = node.getValue(columns[i])) != null) {
@@ -867,6 +883,9 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
             if (!omitTrailingNulls) {
                 n = values.length - 1;
             }
+            /*
+             * Format the values that we fetched in above loop.
+             */
             for (int i=0; i<=n; i++) {
                 if (i != 0) {
                     // We have a TableAppender instance if and only if there is 2 or more columns.
@@ -891,13 +910,21 @@ public class TreeTableFormat extends TabularFormat<TreeTable> {
              */
             final boolean omitCheck = node.getClass().isAnnotationPresent(Acyclic.class);
             if (omitCheck || recursivityGuard.add(node)) {
+                boolean needLineSeparator = multiLineCells;
+                final String lineSeparator = needLineSeparator ? getLineSeparator() : null;
                 final Iterator<? extends TreeTable.Node> it = node.getChildren().iterator();
                 TreeTable.Node next = next(it);
                 while (next != null) {
                     final TreeTable.Node child = next;
                     next = next(it);
-                    isLast[level] = (next == null);                 // Must be set before the call to 'format' below.
-                    format(child, level+1);
+                    needLineSeparator |= (isLast[level] = (next == null));
+                    if (needLineSeparator && lineSeparator != null) {
+                        setLineSeparator(lineSeparator + getTreeSymbols(true, isLast[level]));
+                    }
+                    format(child, level+1);                     // 'isLast' must be set before to call this method.
+                }
+                if (lineSeparator != null) {
+                    setLineSeparator(lineSeparator);            // Restore previous state.
                 }
                 if (!omitCheck && !recursivityGuard.remove(node)) {
                     /*
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/MonolineFormatter.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/MonolineFormatter.java
index 2add892..e5fe846 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/MonolineFormatter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/MonolineFormatter.java
@@ -19,6 +19,7 @@ package org.apache.sis.util.logging;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.text.FieldPosition;
 import java.text.MessageFormat;
 import java.text.SimpleDateFormat;
@@ -773,7 +774,7 @@ loop:   for (int i=0; ; i++) {
                 }
                 writer.flush();
             } catch (IOException e) {
-                throw new AssertionError(e);
+                throw new UncheckedIOException(e);
             }
             /*
              * We wrote the main content, but maybe with some extra lines. Trim the last lines by skipping white spaces
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 9818540..5f20a54 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -192,6 +192,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short Correlation = 19;
 
         /**
+         * Coverage domain
+         */
+        public static final short CoverageDomain = 162;
+
+        /**
          * Current date and time
          */
         public static final short CurrentDateTime = 20;
@@ -687,6 +692,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short RootMeanSquare = 91;
 
         /**
+         * Sample dimensions
+         */
+        public static final short SampleDimensions = 163;
+
+        /**
          * Scale
          */
         public static final short Scale = 92;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index fe381c3..b3ff71a 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -41,6 +41,7 @@ Conversion              = Conversion
 Coordinate              = Coordinate
 CoordinateRefSys        = Coordinate reference system
 Correlation             = Correlation
+CoverageDomain          = Coverage domain
 CurrentDateTime         = Current date and time
 CurrentDirectory        = Current directory
 CycleOmitted            = Cycle omitted
@@ -140,6 +141,7 @@ RepresentativeValue     = Representative value
 Resolution              = Resolution
 Root                    = Root
 RootMeanSquare          = Root Mean Square
+SampleDimensions        = Sample dimensions
 Scale                   = Scale
 SlashSeparatedList_2    = {0}/{1}
 Source                  = Source
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 60d3d4e..5c4f7d5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -48,6 +48,7 @@ Conversion              = Conversion
 Coordinate              = Coordonn\u00e9e
 CoordinateRefSys        = Syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es
 Correlation             = Corr\u00e9lation
+CoverageDomain          = Domaine de la couverture de donn\u00e9es
 CurrentDateTime         = Date et heure courantes
 CurrentDirectory        = R\u00e9pertoire courant
 CycleOmitted            = Cycle omit
@@ -147,6 +148,7 @@ RepresentativeValue     = Valeur repr\u00e9sentative
 Resolution              = R\u00e9solution
 Root                    = Racine
 RootMeanSquare          = Moyenne quadratique
+SampleDimensions        = Dimensions d\u2019\u00e9chantillonnage
 Scale                   = \u00c9chelle
 SlashSeparatedList_2    = {0}/{1}
 Source                  = Source
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/TestSuite.java b/core/sis-utility/src/test/java/org/apache/sis/test/TestSuite.java
index df3d8dc..d6c40eb 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/TestSuite.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/TestSuite.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.io.File;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URL;
 import java.net.URISyntaxException;
 import org.apache.sis.internal.system.Shutdown;
@@ -273,11 +274,11 @@ public abstract strictfp class TestSuite {
         if (!skipShutdown) {
             skipShutdown = true;
             TestCase.LOGGER.removeHandler(LogRecordCollector.INSTANCE);
-            System.err.flush();   // Flushs log messages sent by ConsoleHandler.
+            System.err.flush();                                         // Flushs log messages sent by ConsoleHandler.
             try {
                 LogRecordCollector.INSTANCE.report(System.out);
-            } catch (IOException e) {   // Should never happen.
-                throw new AssertionError(e);
+            } catch (IOException e) {                                   // Should never happen.
+                throw new UncheckedIOException(e);
             }
             SystemListener.fireClasspathChanged();
             Shutdown.stop(TestSuite.class);
diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/collection/CacheTest.java b/core/sis-utility/src/test/java/org/apache/sis/util/collection/CacheTest.java
index 7ea2c7c..f11396f 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/util/collection/CacheTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/util/collection/CacheTest.java
@@ -22,6 +22,7 @@ import java.util.AbstractMap.SimpleEntry;
 import java.util.concurrent.atomic.AtomicReference;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.io.UncheckedIOException;
 
 import org.apache.sis.math.Statistics;
 import org.apache.sis.math.StatisticsFormat;
@@ -314,7 +315,7 @@ public final strictfp class CacheTest extends TestCase {
         try {
             format.format(new Statistics[] {beforeGC, afterGC}, out);
         } catch (IOException e) {
-            throw new AssertionError(e);
+            throw new UncheckedIOException(e);
         }
         assertTrue("Minimum key value should be greater after garbage collection.",
                 afterGC.minimum() >= beforeGC.minimum());
diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/collection/TreeTableFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/util/collection/TreeTableFormatTest.java
index 2a94223..baa7545 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/util/collection/TreeTableFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/util/collection/TreeTableFormatTest.java
@@ -34,7 +34,7 @@ import static org.apache.sis.util.collection.TableColumn.*;
  * Tests the {@link TreeTableFormat} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.3
+ * @version 1.0
  * @since   0.3
  * @module
  */
@@ -266,4 +266,35 @@ public final strictfp class TreeTableFormatTest extends TestCase {
                 "  ├─Enum……………… Half down\n" +                      // No localization provided.
                 "  └─i18n……………… 日本語の言葉\n", tf.format(table));
     }
+
+    /**
+     * Tests formatting of a tree with multi-lines values.
+     * This is supported only for tree made of a single column.
+     */
+    @Test
+    public void testMultiLinesTree() {
+        final TableColumn<String> value = new TableColumn<>(String.class, "value");
+        final DefaultTreeTable table = new DefaultTreeTable(value);
+        final TreeTable.Node   root  = new DefaultTreeTable.Node(table);
+        root.setValue(value, "Value #1");
+        final TreeTable.Node branch1 = new DefaultTreeTable.Node(table);
+        branch1.setValue(value, "Value #2");
+        root.getChildren().add(branch1);
+        final TreeTable.Node branch2 = new DefaultTreeTable.Node(table);
+        branch2.setValue(value, "Value #3");
+        root.getChildren().add(branch2);
+        final TreeTable.Node leaf = new DefaultTreeTable.Node(table);
+        leaf.setValue(value, "val #4\twith tab\nand a new line");
+        branch1.getChildren().add(leaf);
+        table.setRoot(root);
+
+        final TreeTableFormat tf = new TreeTableFormat(null, null);
+        tf.setVerticalLinePosition(1);
+        assertMultilinesEquals(
+                "Value #1\n" +
+                " ├──Value #2\n" +
+                " │   └──val #4  with tab\n" +
+                " │      and a new line\n" +
+                " └──Value #3\n", tf.format(table));
+    }
 }
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index d148336..c3d55ae 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -27,6 +27,7 @@ import java.util.logging.LogRecord;
 import java.util.NoSuchElementException;
 import java.lang.reflect.Array;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import javax.measure.Unit;
 import javax.measure.Quantity;
 import javax.measure.UnitConverter;
@@ -1665,7 +1666,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
         try {
             table.flush();
         } catch (IOException e) {
-            throw new AssertionError(e);        // Should never happen since we wrote to a StringBuffer.
+            throw new UncheckedIOException(e);          // Should never happen since we wrote to a StringBuffer.
         }
         return buffer.toString();
     }
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Metadata.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Metadata.java
index 01d4c9d..6077ce7 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Metadata.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Metadata.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import javax.xml.bind.annotation.XmlList;
 import javax.xml.bind.annotation.XmlElement;
 
@@ -481,7 +482,7 @@ public final class Metadata extends SimpleMetadata {
         try {
             table.flush();
         } catch (IOException e) {
-            throw new AssertionError(e);        // Should never happen since we are writing to a StringBuilder.
+            throw new UncheckedIOException(e);      // Should never happen since we are writing to a StringBuilder.
         }
         return buffer.toString();
     }
diff --git a/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/WriterTest.java b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/WriterTest.java
index 4b39b7d..926458f 100644
--- a/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/WriterTest.java
+++ b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/WriterTest.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Arrays;
 import java.time.Instant;
 import java.net.URI;
+import java.io.UncheckedIOException;
 import java.io.ByteArrayOutputStream;
 import java.io.UnsupportedEncodingException;
 import com.esri.core.geometry.Point;
@@ -98,7 +99,7 @@ public final strictfp class WriterTest extends TestCase {
         try {
             return output.toString("UTF-8");
         } catch (UnsupportedEncodingException e) {
-            throw new AssertionError(e);
+            throw new UncheckedIOException(e);
         }
     }
 


Mime
View raw message