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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f7fc74b400 Reuse the code of `TileMatrixSet.toString()` in the JavaFX 
widget. It produces a table easier to read.
f7fc74b400 is described below

commit f7fc74b4004d327a9016d021ce0125b292351101
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Feb 25 19:07:41 2026 +0100

    Reuse the code of `TileMatrixSet.toString()` in the JavaFX widget.
    It produces a table easier to read.
---
 .../main/org/apache/sis/feature/FeatureFormat.java |   8 +-
 .../apache/sis/storage/tiling/ImagePyramid.java    |   7 +-
 .../apache/sis/storage/tiling/ImageTileMatrix.java |   9 +-
 .../sis/storage/tiling/TileMatrixFormatter.java    | 310 -------------
 .../sis/storage/tiling/TileMatrixSetFormat.java    | 504 +++++++++++++++++++++
 .../main/org/apache/sis/io/TabularFormat.java      |   4 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  16 +-
 .../apache/sis/gui/coverage/TileMatrixSetPane.java | 304 +++++++------
 .../apache/sis/gui/internal/AlignedTableCell.java  |  87 ++++
 9 files changed, 775 insertions(+), 474 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureFormat.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureFormat.java
index 5414533597..4d0fcc8aa5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureFormat.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureFormat.java
@@ -64,7 +64,8 @@ import org.opengis.feature.Operation;
 
 /**
  * Formats {@linkplain AbstractFeature features} or {@linkplain 
DefaultFeatureType feature types} in a tabular format.
- * This format assumes a monospaced font and an encoding supporting drawing 
box characters (e.g. UTF-8).
+ * This format assumes a monospaced font and a character encoding which 
supports the drawing of box characters,
+ * such as <abbr>UTF</abbr>-8.
  *
  * <h2>Example</h2>
  * A feature named “City” and containing 3 properties (“name”, “population” 
and “twin town”)
@@ -83,7 +84,7 @@ import org.opengis.feature.Operation;
  *
  * <h2>Limitations</h2>
  * <ul>
- *   <li>The current implementation can only format features — parsing is not 
yet implemented.</li>
+ *   <li>The current implementation can only format features — parsing is not 
supported.</li>
  *   <li>{@code FeatureFormat}, like most {@code java.text.Format} subclasses, 
is not thread-safe.</li>
  * </ul>
  *
@@ -759,10 +760,11 @@ format:                     for (final AttributeType<?> 
ct : ((AttributeType<?>)
     }
 
     /**
-     * Not yet supported.
+     * Not supported.
      *
      * @return currently never return.
      * @throws ParseException currently always thrown.
+     * @hidden Not implemented.
      */
     @Override
     public Object parse(final CharSequence text, final ParsePosition pos) 
throws ParseException {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
index 5f5b8f3b5c..da376fb1f8 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
@@ -34,7 +34,6 @@ import 
org.apache.sis.coverage.grid.IncompleteGridGeometryException;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.base.StoreUtilities;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.logging.Logging;
@@ -194,7 +193,7 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
                 level = provider.levelOfIdentifier(tip.toString());
             } catch (IllegalArgumentException e) {
                 if (required) throw e;
-                Logging.ignorableException(StoreUtilities.LOGGER, 
ImagePyramid.class, "indexOf", e);
+                Logging.ignorableException(ImageTileMatrix.LOGGER, 
ImagePyramid.class, "indexOf", e);
                 return -1;
             }
             if (level >= lowerMatrixIndex && level < upperMatrixIndex) {
@@ -446,9 +445,9 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     @Override
     public String toString() {
-        final var f = new TileMatrixFormatter(locale);
+        final var f = new TileMatrixSetFormat(locale, null);
         synchronized (matrices) {
-            return f.format(this);
+            return f.format(this, true);
         }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
index 82e4bec8da..fdda986dba 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java
@@ -23,6 +23,7 @@ import java.util.stream.StreamSupport;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import java.nio.file.Path;
+import java.util.logging.Logger;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.matrix.Matrices;
@@ -34,7 +35,6 @@ import org.apache.sis.storage.NoSuchDataException;
 import org.apache.sis.storage.UnsupportedQueryException;
 import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.base.StoreUtilities;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridCoverage2D;
@@ -63,6 +63,11 @@ import org.apache.sis.storage.internal.Resources;
  * @author  Martin Desruisseaux (Geomatys)
  */
 final class ImageTileMatrix implements TileMatrix {
+    /**
+     * Logger for the tiling package.
+     */
+    static final Logger LOGGER = 
Logger.getLogger("org.apache.sis.storage.tiling");
+
     /**
      * An alphanumeric identifier which is unique in the {@code TileMatrixSet} 
that contains this {@code TileMatrix}.
      * The identifier contains the zoom level as a number encoded in 
<abbr>ASCII</abbr>.
@@ -267,7 +272,7 @@ final class ImageTileMatrix implements TileMatrix {
             }
             return TileStatus.UNKNOWN;
         } catch (ArithmeticException e) {
-            Logging.ignorableException(StoreUtilities.LOGGER, 
ImageTileMatrix.class, "getTileStatus", e);
+            Logging.ignorableException(LOGGER, ImageTileMatrix.class, 
"getTileStatus", e);
         }
         return TileStatus.OUTSIDE_EXTENT;
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixFormatter.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixFormatter.java
deleted file mode 100644
index 21b7123042..0000000000
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixFormatter.java
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.storage.tiling;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Locale;
-import java.util.ArrayList;
-import java.text.NumberFormat;
-import org.opengis.util.GenericName;
-import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.internal.shared.Numerics;
-import org.apache.sis.util.collection.TableColumn;
-import org.apache.sis.util.collection.TreeTableFormat;
-import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.referencing.IdentifiedObjects;
-import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
-import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
-import org.apache.sis.io.TableAppender;
-
-
-/**
- * Formatter of tile matrix sets.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-final class TileMatrixFormatter {
-    /**
-     * Helper class for string representation of an image pyramids as a table.
-     * Each instance describes one table row for one {@link ImageTileMatrix}.
-     */
-    private static final class Row {
-        /** The tile matrix identifier. */
-        private final String identifier;
-
-        /** The tile matrix resolution in each <abbr>CRS</abbr> dimensions. */
-        private final double[] resolution;
-
-        /** The string representations of the tile matrix resolution. */
-        private final String[] formattedResolution;
-
-        /** The number of tiles in each grid dimension. */
-        private final String[] tileCount;
-
-        /** The tile sizes in pixels. */
-        private final String[] tileSize;
-
-        /**
-         * Creates one row in the table for the given matrix.
-         *
-         * @param  matrix  the matrix for which to store information.
-         * @param  integerFormat  the format to use for integer values.
-         * @throws BackingStoreException if an error occurred while extracting 
information.
-         * @throws IncompleteGridGeometryException if the tiling scheme has 
not extent or resolution.
-         *         Tile matrices with such tiling scheme should not have been 
constructed in first place.
-         */
-        private Row(final TileMatrix matrix, final NumberFormat integerFormat) 
{
-            final GenericName id = matrix.getIdentifier();
-            identifier = (id != null) ? id.toString() : "";
-            resolution = matrix.getResolution();
-            formattedResolution = new String[resolution.length];
-            final GridExtent ge = matrix.getTilingScheme().getExtent();
-            tileCount = new String[ge.getDimension()];
-            for (int i=0; i<tileCount.length; i++) {
-                tileCount[i] = integerFormat.format(ge.getSize(i));
-            }
-            final int[] ts = ImageTileMatrix.getTileSize(matrix);
-            tileSize = new String[ts != null ? ts.length : 0];
-            for (int i=0; i<tileSize.length; i++) {
-                tileSize[i] = integerFormat.format(ts[i]);
-            }
-        }
-
-        /**
-         * Creates the string representation of resolutions.
-         */
-        final void formatResolutions(final NumberFormat[] formats) {
-            for (int i=0; i<resolution.length; i++) {
-                formattedResolution[i] = formats[i].format(resolution[i]);
-            }
-        }
-    }
-
-    /**
-     * The locale specified at construction time. May be {@code null}.
-     */
-    private final Locale locale;
-
-    /**
-     * Resources for table header.
-     */
-    private final Vocabulary vocabulary;
-
-    /**
-     * The object to use for formatting integer values.
-     */
-    private final NumberFormat integerFormat;
-
-    /**
-     * The error that occurred while formatting a value.
-     */
-    private Throwable error;
-
-    /**
-     * Creates a new formatter using the given locale.
-     */
-    TileMatrixFormatter(final Locale locale) {
-        this.locale = locale;
-        vocabulary = Vocabulary.forLocale(locale);
-        integerFormat = (locale != null)
-                ? NumberFormat.getIntegerInstance(locale)
-                : NumberFormat.getIntegerInstance();
-    }
-
-    /**
-     * Returns a string representation of the given tile matrices.
-     * Each tile matrix is formatted as a row in a table.
-     *
-     * @param  matrices  the tile matrices to format.
-     * @return the string representation of the table of tile matrices.
-     */
-    final String format(final TileMatrixSet matrices) {
-        final var buffer = new StringBuilder(1000);
-        try {
-            formatHeader(matrices, buffer);
-            formatTable(matrices, buffer);
-            if (error == null) {
-                return buffer.toString();
-            }
-            final var writer = new 
StringWriter(buffer.length()).append(buffer);
-            vocabulary.appendLabel(Vocabulary.Keys.Warnings, writer);
-            error.printStackTrace(new PrintWriter(writer.append(' ')));
-            return writer.append(System.lineSeparator()).toString();
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
-        }
-    }
-
-    /**
-     * Formats the header of the Tile Matrix Set (<abbr>TMS</abbr>) 
representation.
-     * The header contains the <abbr>TMS</abbr> identifier, the Coordinate 
Reference System
-     * and the geographic bounding box.
-     *
-     * @param  matrices  the Tile Matrix Set (<abbr>TMS</abbr>) to format.
-     * @param  buffer    where to format the header.
-     * @throws IOException should never happen but handled by the caller for 
convenience.
-     */
-    private void formatHeader(final TileMatrixSet matrices, final 
StringBuilder buffer) throws IOException {
-        
buffer.append(vocabulary.getString(Vocabulary.Keys.TileMatrixSets)).append(' ')
-              .append(vocabulary.getString(Vocabulary.Keys.Quoted_1, 
matrices.getIdentifier()))
-              .append(System.lineSeparator());
-        try {
-            final String crs = 
IdentifiedObjects.getDisplayName(matrices.getCoordinateReferenceSystem(), 
locale);
-            if (crs != null) {
-                vocabulary.appendLabel(Vocabulary.Keys.ReferenceSystem, 
buffer);
-                buffer.append(' ').append(crs).append(System.lineSeparator());
-            }
-            matrices.getEnvelope().ifPresent((envelope) -> {
-                try {
-                    final var bbox = new DefaultGeographicBoundingBox();
-                    bbox.setBounds(envelope);
-                    bbox.setInclusion(null);
-                    final var mf = new TreeTableFormat(locale, null);
-                    mf.setColumns(TableColumn.NAME, TableColumn.VALUE);
-                    buffer.append(mf.format(bbox.asTreeTable()));
-                } catch (TransformException e) {
-                    addError(e);
-                }
-            });
-        } catch (BackingStoreException e) {
-            addError(e.getCause());
-        }
-    }
-
-    /**
-     * Updates an array of maximal length of string representations in the 
given columns.
-     * The {@code lenghts} array is updated in-place.
-     */
-    private static void updateMaximalLengths(final int[] lengths, final 
String[] columns) {
-        for (int i = Math.min(lengths.length, columns.length); --i >= 0;) {
-            final int length = columns[i].length();
-            if (length > lengths[i]) {
-                lengths[i] = length;
-            }
-        }
-    }
-
-    /**
-     * Appends spaces in front of the given columns in order to have the 
specified lengths.
-     */
-    private static void rightAlign(final int[] lengths, final String[] 
columns) {
-        for (int i = Math.min(lengths.length, columns.length); --i >= 0;) {
-            final String column = columns[i];
-            final int more = lengths[i] - column.length();
-            if (more > 0) {
-                columns[i] = CharSequences.spaces(more) + columns[i];
-            }
-        }
-    }
-
-    /**
-     * Formats the main body of the Tile Matrix Set (<abbr>TMS</abbr>) 
representation.
-     * This is formatted as a table.
-     *
-     * @param  matrices  the Tile Matrix Set (<abbr>TMS</abbr>) to format.
-     * @param  buffer    where to format the main body.
-     * @throws IOException should never happen but handled by the caller for 
convenience.
-     */
-    private void formatTable(final TileMatrixSet matrices, final StringBuilder 
buffer) throws IOException {
-        final var rows = new ArrayList<Row>();
-        int crsDimension = 0, gridDimension = 0, sizeDimension = 0;
-        for (final TileMatrix matrix : matrices.getTileMatrices().values()) 
try {
-            final var row = new Row(matrix, integerFormat);
-            crsDimension  = Math.max(crsDimension,  
row.formattedResolution.length);
-            gridDimension = Math.max(gridDimension, row.tileCount.length);
-            sizeDimension = Math.max(sizeDimension, row.tileSize.length);
-            rows.add(row);  // Add only on success.
-        } catch (RuntimeException e) {
-            addError(e);
-        }
-        /*
-         * Find the number of fraction digits to use for showing the 
resolution.
-         */
-        final var values  = new double[rows.size()];
-        final var formats = new NumberFormat[crsDimension];
-        for (int i=0; i<crsDimension; i++) {
-            for (int j=0; j<values.length; j++) {
-                values[j] = rows.get(j).resolution[i];
-            }
-            final NumberFormat format = (locale != null)
-                    ? NumberFormat.getNumberInstance(locale)
-                    : NumberFormat.getNumberInstance();
-            final int n = Numerics.suggestFractionDigits(values);
-            format.setMinimumFractionDigits(n);
-            format.setMaximumFractionDigits(n);
-            formats[i] = format;
-        }
-        /*
-         * At this point, all values have been formatted as character strings.
-         * Compute the maximum lengths in each column in order to align the 
values.
-         */
-        final int[] resolutionLengths = new int[crsDimension];
-        final int[]  tileCountLengths = new int[gridDimension];
-        final int[]   tileSizeLengths = new int[sizeDimension];
-        for (final Row row : rows) {
-            row.formatResolutions(formats);
-            updateMaximalLengths(resolutionLengths, row.formattedResolution);
-            updateMaximalLengths(tileCountLengths,  row.tileCount);
-            updateMaximalLengths(tileSizeLengths,   row.tileSize);
-        }
-        for (final Row row : rows) {
-            rightAlign(resolutionLengths, row.formattedResolution);
-            rightAlign(tileCountLengths,  row.tileCount);
-            rightAlign(tileSizeLengths,   row.tileSize);
-        }
-        /*
-         * All data are prepared. Write the table.
-         */
-        final var table = new TableAppender(buffer);
-        table.appendHorizontalSeparator();
-        
table.append(vocabulary.getString(Vocabulary.Keys.Identifier)).nextColumn();
-        if  (crsDimension != 0) 
table.append(vocabulary.getString(Vocabulary.Keys.Resolution)).nextColumn();
-        if (gridDimension != 0) 
table.append(vocabulary.getString(Vocabulary.Keys.TileCount)) .nextColumn();
-        if (sizeDimension != 0) 
table.append(vocabulary.getString(Vocabulary.Keys.TileSize))  .nextColumn();
-        table.appendHorizontalSeparator();
-        for (final Row row : rows) {
-            table.setCellAlignment(TableAppender.ALIGN_LEFT);
-            table.append(row.identifier).nextColumn();
-            table.setCellAlignment(TableAppender.ALIGN_RIGHT);
-            if  (crsDimension != 0) table.append(String.join(" × ", 
row.formattedResolution)).nextColumn();
-            if (gridDimension != 0) table.append(String.join(" × ", 
row.tileCount)) .nextColumn();
-            if (sizeDimension != 0) table.append(String.join(" × ", 
row.tileSize))  .nextColumn();
-            table.nextLine();
-        }
-        table.appendHorizontalSeparator();
-        table.flush();
-    }
-
-    /**
-     * Records that an error occurred.
-     *
-     * @param  e  the error that occurred.
-     */
-    private void addError(final Throwable e) {
-        if (error == null) {
-            error = e;
-        } else {
-            error.addSuppressed(e);
-        }
-    }
-}
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSetFormat.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSetFormat.java
new file mode 100644
index 0000000000..38049ca48a
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSetFormat.java
@@ -0,0 +1,504 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.tiling;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.text.Format;
+import java.util.Locale;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Arrays;
+import java.util.StringJoiner;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import org.opengis.util.GenericName;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.internal.shared.Numerics;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTableFormat;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.io.CompoundFormat;
+import org.apache.sis.io.TableAppender;
+
+
+/**
+ * Formats Tile Matrix Sets (<abbr>TMS</abbr>) in a tabular format.
+ * This format assumes a monospaced font and a character encoding which 
supports the drawing of box characters,
+ * such as <abbr>UTF</abbr>-8.
+ *
+ * <h2>Limitations</h2>
+ * <ul>
+ *   <li>The current implementation can only format tile matrices — parsing is 
supported.</li>
+ *   <li>{@code TileMatrixSetFormat}, like most {@code java.text.Format} 
subclasses, is not thread-safe.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.7
+ * @since   1.7
+ */
+public class TileMatrixSetFormat extends CompoundFormat<TileMatrixSet> {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 2612127094991996016L;
+
+    /**
+     * Helper class for string representation of an image pyramid as a table.
+     * Each instance describes one table row for one {@link ImageTileMatrix}.
+     */
+    private static final class Row {
+        /** The tile matrix identifier. */
+        final String identifier;
+
+        /** The tile matrix resolution in each <abbr>CRS</abbr> dimensions. */
+        final double[] resolution;
+
+        /** The string representations of the tile matrix resolution. */
+        final String[] formattedResolution;
+
+        /** The number of tiles in each grid dimension. */
+        final String[] tileCount;
+
+        /** The tile sizes in pixels. */
+        final String[] tileSize;
+
+        /** The extent of the tiling scheme. */
+        private final GridExtent tilingScheme;
+
+        /**
+         * Creates one row in the table for the given matrix.
+         *
+         * @param  matrix         the matrix for which to store information.
+         * @param  integerFormat  a number format configured for integer 
values.
+         * @throws BackingStoreException if an error occurred while extracting 
information.
+         * @throws IncompleteGridGeometryException if the tiling scheme has 
not extent or resolution.
+         *         Tile matrices with such tiling scheme should not have been 
constructed in first place.
+         */
+        Row(final TileMatrix matrix, final NumberFormat integerFormat) {
+            final GenericName id = matrix.getIdentifier();
+            identifier = (id != null) ? id.toString() : "";
+            resolution = matrix.getResolution();
+            formattedResolution = new String[resolution.length];
+            tilingScheme = matrix.getTilingScheme().getExtent();
+            tileCount = new String[tilingScheme.getDimension()];
+            for (int i=0; i<tileCount.length; i++) {
+                tileCount[i] = integerFormat.format(tilingScheme.getSize(i));
+            }
+            final int[] ts = ImageTileMatrix.getTileSize(matrix);
+            tileSize = new String[ts != null ? ts.length : 0];
+            for (int i=0; i<tileSize.length; i++) {
+                tileSize[i] = integerFormat.format(ts[i]);
+            }
+        }
+
+        /**
+         * Updates the axis name in the dimension <var>i</var> of the grid 
extent.
+         * This method verifies that all grid dimensions have the same name.
+         */
+        private final void searchCommonAxisName(final String[] gridAxes, final 
int i) {
+            tilingScheme.getAxisType(i).ifPresent((axis) -> {
+                final String name = axis.identifier().orElseGet(() -> 
axis.name().toLowerCase(Locale.US));
+                final String current = gridAxes[i];
+                if (current == null) {
+                    gridAxes[i] = name;
+                } else if (!current.equals(name)) {
+                    gridAxes[i] = "";
+                }
+            });
+        }
+
+        /**
+         * Creates the string representation of the resolutions in all 
resolution columns of all rows.
+         * This method computes the number of fraction digits based on the 
resolution values of all rows.
+         *
+         * @param  rows    the rows in which to format the resolutions.
+         * @param  format  the number formats to use. Its number of fraction 
digits will be updated.
+         * @return the maximal number of resolution values found in all rows.
+         *         This value should be equal to the number of dimensions in 
the <abbr>CRS</abbr>.
+         */
+        static int formatResolutions(final List<Row> rows, final NumberFormat 
format) {
+            final var values = new double[rows.size()];
+            for (int i=0; ; i++) {
+                int count = 0;
+                for (final Row row : rows) {
+                    if (i < row.resolution.length) {
+                        values[count++] = row.resolution[i];
+                    }
+                }
+                if (count == 0) return i;
+                final int n = 
Numerics.suggestFractionDigits(ArraysExt.resize(values, count));
+                format.setMinimumFractionDigits(n);
+                format.setMaximumFractionDigits(n);
+                for (final Row row : rows) {
+                    if (i < row.resolution.length) {
+                        row.formattedResolution[i] = 
format.format(row.resolution[i]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * The error that occurred while formatting a Tile Matrix Set.
+     */
+    private transient Throwable error;
+
+    /**
+     * Creates a new formatter using the given locale and timezone.
+     *
+     * @param  locale    the locale for the new {@code Format}, or {@code 
null} for {@code Locale.ROOT}.
+     * @param  timezone  the timezone, or {@code null} for UTC.
+     */
+    public TileMatrixSetFormat(final Locale locale, final TimeZone timezone) {
+        super(locale, timezone);
+    }
+
+    /**
+     * Returns the type of values formatted by this {@code Format} instance.
+     *
+     * @return the type of values formatted by this {@code Format} instance.
+     */
+    @Override
+    public final Class<TileMatrixSet> getValueType() {
+        return TileMatrixSet.class;
+    }
+
+    /**
+     * Formats the properties of the given Tile Matrix Set for presentation 
before the main table.
+     * The properties are formatted as {@link String}s using the locale given 
at construction time.
+     * The returned map contains the following entries if the corresponding 
properties were found:
+     *
+     * <table class="sis">
+     *   <caption>Tile Matrix Set (<abbr>TMS</abbr>) formatted 
properties</caption>
+     *   <tr><th>Key</th>                  <th>Value type</th>                 
          <th>Description</th></tr>
+     *   <tr><td>{@code "identifier"}</td> <td>{@link String}</td>             
          <td>Identifier of the <abbr>TMS</abbr>.</td></tr>
+     *   <tr><td>{@code "crsName"}</td>    <td>{@link String}</td>             
          <td>Name of the <abbr>CRS</abbr>.</td></tr>
+     *   <tr><td>{@code "crs"}</td>        <td>{@link 
CoordinateReferenceSystem}</td>    <td>The <abbr>CRS</abbr>.</td></tr>
+     *   <tr><td>{@code "bbox"}</td>       <td>{@link 
DefaultGeographicBoundingBox}</td> <td>Bounding box of the 
<abbr>TMS</abbr>.</td></tr>
+     * </table>
+     *
+     * The returned properties can be completed by a call to {@link 
#formatTable(Iterable, Map)}.
+     *
+     * @param  matrices  the tile matrices to format.
+     * @return properties of the header of the given tile matrix set.
+     */
+    public Map<String, Object> formatHeader(final TileMatrixSet matrices) {
+        final var addTo = new HashMap<String, Object>();
+        addTo.put("identifier", matrices.getIdentifier());
+        try {
+            final CoordinateReferenceSystem crs = 
matrices.getCoordinateReferenceSystem();
+            addTo.put("crsName", IdentifiedObjects.getDisplayName(crs, 
getLocale()));
+            addTo.put("crs", crs);
+            matrices.getEnvelope().ifPresent((envelope) -> {
+                try {
+                    final var bbox = new DefaultGeographicBoundingBox();
+                    bbox.setBounds(envelope);
+                    bbox.setInclusion(null);
+                    addTo.put("bbox", bbox);
+                } catch (TransformException e) {
+                    // Ignore because this exception may be normal if the 
envelope has no spatial component.
+                    Logging.ignorableException(ImageTileMatrix.LOGGER, 
TileMatrixSetFormat.class, "format", e);
+                }
+            });
+        } catch (BackingStoreException e) {
+            // The CRS or envelope may be missing in the header, but we may 
still be able to format the table.
+            addError(e.getCause());
+        }
+        addTo.values().removeIf(Objects::isNull);
+        return addTo;
+    }
+
+    /**
+     * Formats the properties of the given Tile Matrices in a way suitable to 
a tabular format.
+     * The properties are formatted as {@link String}s using the locale given 
at construction time.
+     * The returned map contains the following entries if the corresponding 
properties were found:
+     *
+     * <table class="sis">
+     *   <caption>Tile Matrices formatted properties</caption>
+     *   <tr><th>Key</th>                   <th>Value type</th>         
<th>Description</th></tr>
+     *   <tr><td>{@code "identifiers"}</td> <td>{@code String[]}</td>   
<td>Column of the identifier of each Tile Matrix.</td></tr>
+     *   <tr><td>{@code "resolutions"}</td> <td>{@code String[][]}</td> 
<td>Columns of the resolution of each Tile Matrix.</td></tr>
+     *   <tr><td>{@code "tileCounts"}</td>  <td>{@code String[][]}</td> 
<td>Columns of the number of tiles of each Tile Matrix.</td></tr>
+     *   <tr><td>{@code "tileSizes"}</td>   <td>{@code String[][]}</td> 
<td>Columns of the tile size of each Tile Matrix.</td></tr>
+     *   <tr><td>{@code "gridAxes"}</td>    <td>{@code String[]}</td>   
<td>Name of grid axes.</td></tr>
+     * </table>
+     *
+     * The {@code "tileSizes"} property may be absent if not applicable.
+     * Implementations other than the default implementation may also choose 
to add or remove more properties.
+     *
+     * <p>This method returns the number of rows in the table to format.
+     * The length of the {@code "identifiers"} array is that number of rows.
+     * All other arrays have a length equal to the number of columns in a 
group of columns,
+     * which is 2 in the usual case of two-dimensional Tile Matrix Sets.
+     * The {@code "resolutions[i]"}, {@code "tileCounts[i]"} and {@code 
"tileSizes[i]"} arrays,
+     * where <var>i</var> is a column index, all have a length equal to the 
number of rows.
+     * Some element may be {@code null} if the corresponding data could not be 
extracted.</p>
+     *
+     * @param  matrices  the tile matrices to format.
+     * @param  addTo     where to put the properties of the given tile matrix 
set.
+     * @return number of rows in the table to format.
+     */
+    public int formatTable(final Iterable<? extends TileMatrix> matrices, 
final Map<String, Object> addTo) {
+        final var rows = new ArrayList<Row>();
+        int gridDimension = 0, sizeDimension = 0;
+        final var integerFormat = (NumberFormat) getFormat(Long.class);
+        for (final TileMatrix matrix : matrices) try {
+            final var row = new Row(matrix, integerFormat);
+            gridDimension = Math.max(gridDimension, row.tileCount.length);
+            sizeDimension = Math.max(sizeDimension, row.tileSize.length);
+            rows.add(row);  // Add only on success.
+        } catch (RuntimeException e) {
+            addError(e);    // Skip the row. Next row will be tried.
+        }
+        final int crsDimension = Row.formatResolutions(rows, (NumberFormat) 
getFormat(Double.class));
+        final int numRows     = rows.size();
+        final var identifiers = new String[numRows];
+        final var resolutions = new String[ crsDimension][numRows];
+        final var tileCounts  = new String[gridDimension][numRows];
+        final var tileSizes   = new String[sizeDimension][numRows];
+        final var gridAxes    = new String[gridDimension];
+        for (int j=0; j<numRows; j++) {
+            final Row row = rows.get(j);
+            identifiers[j] = row.identifier;
+            for (int i = Math.min(crsDimension, row.resolution.length); --i >= 
0;) {
+                resolutions[i][j] = row.formattedResolution[i];
+            }
+            for (int i = Math.min(gridDimension, row.tileCount.length); --i >= 
0;) {
+                tileCounts[i][j] = row.tileCount[i];
+                row.searchCommonAxisName(gridAxes, i);
+            }
+            for (int i = Math.min(sizeDimension, row.tileSize.length); --i >= 
0;) {
+                tileSizes[i][j] = row.tileSize[i];
+            }
+        }
+        if (numRows       != 0) addTo.put("identifiers", identifiers);
+        if (crsDimension  != 0) addTo.put("resolutions", resolutions);
+        if (gridDimension != 0) addTo.put("tileCounts",  tileCounts);
+        if (sizeDimension != 0) addTo.put("tileSizes",   tileSizes);
+        for (int i=0; i<gridAxes.length; i++) {
+            final String name = gridAxes[i];
+            if (name != null && name.isBlank()) {
+                gridAxes[i] = null;
+            }
+        }
+        if (!ArraysExt.allEquals(gridAxes, null)) {
+            addTo.put("gridAxes", gridAxes);
+        }
+        return numRows;
+    }
+
+    /**
+     * Returns localized resources for the table header.
+     */
+    private Vocabulary vocabulary() {
+        return Vocabulary.forLocale(getLocale());
+    }
+
+    /**
+     * Formats a textual representation of the given tile matrices.
+     * This method delegates to {@link #format(TileMatrixSet, Appendable)}
+     * with a temporary buffer.
+     *
+     * @param  matrices  the tile matrices to format.
+     * @return a string representation of the given Tile Matrix Set.
+     */
+    public String format(TileMatrixSet matrices) {
+        return format(matrices, false);
+    }
+
+    /**
+     * Formats the given tile matrices, optionally with a report of exceptions.
+     *
+     * @param  matrices     the tile matrices to format.
+     * @param  reportError  whether to report exceptions.
+     * @return a string representation of the given Tile Matrix Set.
+     */
+    final String format(final TileMatrixSet matrices, final boolean 
reportError) {
+        final var buffer = new StringBuilder(1000);
+        try {
+            format(matrices, buffer);
+            if (!reportError || error == null) {
+                return buffer.toString();
+            }
+            final var writer = new 
StringWriter(buffer.length()).append(buffer);
+            vocabulary().appendLabel(Vocabulary.Keys.Warnings, writer);
+            error.printStackTrace(new PrintWriter(writer.append(' ')));
+            return writer.append(System.lineSeparator()).toString();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * Formats a textual representation of the given tile matrices.
+     * First, a header is formatted with the <abbr>TMS</abbr> identifier,
+     * the Coordinate Reference System and the geographic bounding box.
+     * Then, each tile matrix is formatted as a row in a table.
+     *
+     * @param  matrices    the tile matrices to format.
+     * @param  toAppendTo  where to format the object.
+     * @throws IOException if an error occurred while writing to the given 
appendable.
+     */
+    @Override
+    public void format(final TileMatrixSet matrices, final Appendable 
toAppendTo) throws IOException {
+        final Vocabulary vocabulary = vocabulary();
+        final Map<String, Object> properties = formatHeader(matrices);
+        final int numRows = formatTable(matrices.getTileMatrices().values(), 
properties);
+        Object value = properties.get("identifier");
+        if (value != null) {
+            
toAppendTo.append(vocabulary.getString(Vocabulary.Keys.TileMatrixSets)).append('
 ')
+                      .append(vocabulary.getString(Vocabulary.Keys.Quoted_1, 
value))
+                      .append(System.lineSeparator());
+        }
+        value = properties.get("crsName");
+        if (value != null) {
+            vocabulary.appendLabel(Vocabulary.Keys.ReferenceSystem, 
toAppendTo);
+            toAppendTo.append(' 
').append(value.toString()).append(System.lineSeparator());
+        }
+        value = properties.get("bbox");
+        if (value != null) {
+            final var bbox = (DefaultGeographicBoundingBox) value;
+            final var mf = (TreeTableFormat) 
getFormat(GeographicBoundingBox.class);
+            mf.setColumns(TableColumn.NAME, TableColumn.VALUE);
+            toAppendTo.append(mf.format(bbox.asTreeTable()));
+        }
+        /*
+         * The header was formatted by above code. The following code formats 
the main table.
+         * First, we get the values to format in all columns and prepend the 
number of spaces
+         * needed for right alignment.
+         */
+        final var identifiers = (String[])   properties.get("identifiers");
+        final var resolutions = (String[][]) properties.get("resolutions");
+        final var tileCounts  = (String[][]) properties.get("tileCounts");
+        final var tileSizes   = (String[][]) properties.get("tileSizes");
+        String[][][] columnGroups = {resolutions, tileCounts, tileSizes};
+        columnGroups = ArraysExt.resize(columnGroups, 
ArraysExt.removeNulls(columnGroups));
+        for (final String[][] columns : columnGroups) {
+            for (final String[] column : columns) {
+                
Arrays.stream(column).mapToInt(String::length).max().ifPresent((length) -> {
+                    for (int j=0; j<column.length; j++) {
+                        String e  = column[j];
+                        column[j] = CharSequences.spaces(length - e.length()) 
+ e;
+                    }
+                });
+            }
+        }
+        final var table = new TableAppender(toAppendTo);
+        table.appendHorizontalSeparator();
+        if (identifiers != null) 
table.append(vocabulary.getString(Vocabulary.Keys.Identifier)).nextColumn();
+        if (resolutions != null) 
table.append(vocabulary.getString(Vocabulary.Keys.Resolution)).nextColumn();
+        if (tileCounts  != null) 
table.append(vocabulary.getString(Vocabulary.Keys.TileCount)) .nextColumn();
+        if (tileSizes   != null) 
table.append(vocabulary.getString(Vocabulary.Keys.TileSize))  .nextColumn();
+        table.appendHorizontalSeparator();
+        for (int j=0; j<numRows; j++) {
+            if (identifiers != null) {
+                table.setCellAlignment(TableAppender.ALIGN_LEFT);
+                table.append(identifiers[j]).nextColumn();
+                table.setCellAlignment(TableAppender.ALIGN_RIGHT);
+            }
+            for (final String[][] columns : columnGroups) {
+                final var joiner = new StringJoiner(" × ");
+                for (String[] column : columns) {
+                    joiner.add(column[j]);
+                }
+                table.append(joiner.toString()).nextColumn();
+            }
+            table.nextLine();
+        }
+        table.appendHorizontalSeparator();
+        table.flush();
+    }
+
+    /**
+     * Not supported.
+     *
+     * @return currently never return.
+     * @throws ParseException currently always thrown.
+     * @hidden Not implemented.
+     */
+    @Override
+    public TileMatrixSet parse(CharSequence text, ParsePosition pos) throws 
ParseException {
+        throw new ParseException(Errors.forLocale(getLocale())
+                .getString(Errors.Keys.UnsupportedOperation_1, "parse"), 
pos.getIndex());
+    }
+
+    /**
+     * Creates a new format to use for formatting values of the given type.
+     * This method adds the creation of {@link TreeTableFormat}.
+     *
+     * @param  valueType  the base type of values to parse or format.
+     * @return the format to use for parsing of formatting values of the given 
type, or {@code null} if none.
+     * @hidden the addition compared to parent class are implementation 
details.
+     */
+    @Override
+    protected Format createFormat(final Class<?> valueType) {
+        if (valueType == GeographicBoundingBox.class) {
+            return new TreeTableFormat(getLocale(), getTimeZone());
+        }
+        return super.createFormat(valueType);
+    }
+
+    /**
+     * Records that an error occurred.
+     *
+     * @param  e  the error that occurred.
+     */
+    private void addError(final Throwable e) {
+        if (error == null) {
+            error = e;
+        } else {
+            error.addSuppressed(e);
+        }
+    }
+
+    /**
+     * If an error occurred while formatting the Tile Matrix Set, returns that 
error.
+     *
+     * @return the error that occurred during formatting.
+     */
+    public Optional<Throwable> getError() {
+        return Optional.ofNullable(error);
+    }
+
+    /**
+     * Clears the error status. This method should be invoked if the same 
{@code TileMatrixSetFormat}
+     * instance is used for formatting many {@code TileMatrixSet} instances.
+     */
+    public void clear() {
+        error = null;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TabularFormat.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TabularFormat.java
index fa2cdc28e3..d77210b5c1 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TabularFormat.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TabularFormat.java
@@ -37,7 +37,7 @@ import org.apache.sis.util.resources.Errors;
  *   <li>{@link #setColumnSeparatorPattern(String)}</li>
  * </ul>
  *
- * <h2>Note for subclass implementions</h2>
+ * <h2>Note for subclass implementations</h2>
  * This base class takes care of splitting a column separator pattern into its 
components
  * ({@link #beforeFill}, {@link #fillCharacter} and {@link #columnSeparator})
  * for easier usage in {@code format(…)} method implementations.
@@ -133,7 +133,7 @@ public abstract class TabularFormat<T> extends 
CompoundFormat<T> {
      *                   or {@code null} for the {@linkplain Locale#ROOT root 
locale}.
      * @param  timezone  the timezone, or {@code null} for UTC.
      */
-    public TabularFormat(final Locale locale, final TimeZone timezone) {
+    protected TabularFormat(final Locale locale, final TimeZone timezone) {
         super(locale, timezone);
         beforeFill      = "";
         fillCharacter   = ' ';
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageStyling.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageStyling.java
index 0b52887686..8a862b330f 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageStyling.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -22,8 +22,6 @@ import java.util.HashMap;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.function.Function;
-import javafx.geometry.Pos;
-import javafx.scene.control.TableCell;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableView;
 import javafx.scene.control.MenuItem;
@@ -36,6 +34,7 @@ import org.opengis.util.InternationalString;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.image.Colorizer;
 import org.apache.sis.gui.internal.Resources;
+import org.apache.sis.gui.internal.AlignedTableCell;
 import org.apache.sis.gui.internal.ImmutableObjectProperty;
 import org.apache.sis.gui.controls.ColorRamp;
 import org.apache.sis.gui.controls.ColorColumnHandler;
@@ -201,7 +200,7 @@ final class CoverageStyling extends 
ColorColumnHandler<Category>
     final TableView<Category> createCategoryTable(final Resources resources, 
final Vocabulary vocabulary) {
         final var name = new 
TableColumn<Category,String>(vocabulary.getString(Vocabulary.Keys.Name));
         name.setCellValueFactory(CoverageStyling::getCategoryName);
-        name.setCellFactory(CoverageStyling::createNameCell);
+        name.setCellFactory(AlignedTableCell.centerLeft());
         name.setEditable(false);
         name.setId("name");
         /*
@@ -222,17 +221,6 @@ final class CoverageStyling extends 
ColorColumnHandler<Category>
         return table;
     }
 
-    /**
-     * Invoked for creating a cell for the "name" column.
-     * Returns the JavaFX default cell except for vertical alignment, which is 
centered.
-     */
-    private static TableCell<Category,String> createNameCell(final 
TableColumn<Category,String> column) {
-        @SuppressWarnings("unchecked")
-        final var cell = (TableCell<Category,String>) 
TableColumn.DEFAULT_CELL_FACTORY.call(column);
-        cell.setAlignment(Pos.CENTER_LEFT);
-        return cell;
-    }
-
     /**
      * Invoked when the table needs to render a text in the "Name" column of 
the category table.
      */
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/TileMatrixSetPane.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/TileMatrixSetPane.java
index 27df9994e0..df58248410 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/TileMatrixSetPane.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/TileMatrixSetPane.java
@@ -16,9 +16,11 @@
  */
 package org.apache.sis.gui.coverage;
 
-import java.util.Collection;
+import java.util.Arrays;
 import java.util.Locale;
 import java.util.List;
+import java.util.Map;
+import java.util.function.IntFunction;
 import javafx.collections.ObservableList;
 import javafx.beans.value.ObservableValue;
 import javafx.beans.property.ObjectProperty;
@@ -38,29 +40,30 @@ import javafx.util.StringConverter;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.metadata.spatial.DimensionNameType;
-import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.tiling.TileMatrix;
 import org.apache.sis.storage.tiling.TileMatrixSet;
 import org.apache.sis.storage.tiling.TiledResource;
-import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.storage.tiling.TileMatrixSetFormat;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.gui.Widget;
+import org.apache.sis.gui.internal.AlignedTableCell;
 import org.apache.sis.gui.internal.BackgroundThreads;
 import org.apache.sis.gui.internal.ExceptionReporter;
-import org.apache.sis.util.collection.Containers;
 
 
 /**
  * A visual representation of the internal tile matrices defined in a {@link 
TiledResource}.
+ * Each {@link TileMatrix} instance is presented as a row in a table. The 
columns are the tile
+ * matrix identifier, the resolution, the number of tiles and (if applicable) 
the tile size.
  *
- * @todo change the text area to a split pane with a tree view on the left and 
a description pane on the right
- * @todo if the resource is writable, add tiling modification controls
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.7
  *
- * @author Johann Sorel (Geomatys)
+ * @see TileMatrixSetFormat
  *
- * @sinec 1.7
+ * @since 1.7
  */
 public class TileMatrixSetPane extends Widget {
     /**
@@ -86,12 +89,12 @@ public class TileMatrixSetPane extends Widget {
     };
 
     /**
-     * The locale for texts in controls, or {@code null} for the default 
locale.
+     * The object to use for formatting Tile Matrix Set properties.
      */
-    private final Locale locale;
+    private final TileMatrixSetFormat formatter;
 
     /**
-     * The data shown in this widget.
+     * The resource for which this widget is showing the Tile Matrix Sets 
(<abbr>TMS</abbr>).
      *
      * @see #getContent()
      * @see #setContent(TiledResource)
@@ -104,7 +107,7 @@ public class TileMatrixSetPane extends Widget {
     private final ComboBox<TileMatrixSet> tileMatriceSets;
 
     /**
-     * The label where to write the name of the Coordinate Reference System.
+     * The label where to write the name of the Coordinate Reference System of 
the selected <abbr>TMS</abbr>.
      */
     private final Label crsName;
 
@@ -116,91 +119,106 @@ public class TileMatrixSetPane extends Widget {
     /**
      * The columns for the resolution along each <abbr>CRS</abbr> axis.
      * The number of columns is the number of <abbr>CRS</abbr> dimensions.
+     * Values are {@link Double} numbers, for formatted as {@link String}s
+     * by {@link TileMatrixSetFormat}.
      */
-    private final TableColumn<Row, Double> tileResolutionColumns;
+    private final TableColumn<Row, String> tileResolutionColumns;
 
     /**
      * The columns for the number of tiles in each dimension.
      * The number of columns is the number of grid dimensions.
+     * Values are {@link Long}s, for formatted as {@link String}s by {@link 
TileMatrixSetFormat}.
      */
-    private final TableColumn<Row, Long> tileCountColumns;
+    private final TableColumn<Row, String> tileCountColumns;
+
+    /**
+     * The columns for the tile size in each dimension.
+     * The number of columns is the number of grid dimensions.
+     * Values are {@link Integer}s, for formatted as {@link String}s by {@link 
TileMatrixSetFormat}.
+     */
+    private final TableColumn<Row, String> tileSizeColumns;
 
     /**
      * A row in the table of tile matrices shown by {@link #tileMatrices}.
-     * The resolution and tile count are group of columns.
+     * The resolution, tile count and tile size are groups of columns.
      */
     private static final class Row {
         /**
          * Property for the Tile Matrix identifier.
          */
-        private final SimpleStringProperty identifier;
+        final SimpleStringProperty identifier;
 
         /**
          * Resolution along each <abbr>CRS</abbr> dimension.
          * They are the values to show in the group of {@link 
#tileResolutionColumns}.
          */
-        private final SimpleObjectProperty<Double>[] resolution;
+        final SimpleStringProperty[] resolution;
 
         /**
          * Number of tiles along each grid dimension.
          * They are the values to show in the group {@link #tileCountColumns}.
          */
-        private final SimpleObjectProperty<Long>[] tileCount;
+        final SimpleStringProperty[] tileCount;
 
         /**
-         * Creates a new row for the given tile matrix.
-         *
-         * @param  matrix  the matrix for which to create a row in the table.
+         * Tile size along each grid dimension.
+         * They are the values to show in the group {@link #tileSizeColumns}.
          */
-        @SuppressWarnings({"this-escape", "rawtypes", "unchecked"})
-        Row(final TileMatrix matrix) {
-            identifier = new SimpleStringProperty(this, "identifier");
-            final GenericName id = matrix.getIdentifier();
-            if (id != null) {
-                identifier.setValue(id.toString());
-            }
-            final double[] r = matrix.getResolution();
-            resolution = new SimpleObjectProperty[r.length];
-            for (int i=0; i<resolution.length; i++) {
-                (resolution[i] = new SimpleObjectProperty<>(this, 
"resolution")).set(r[i]);
-            }
-            final GridExtent extent = matrix.getTilingScheme().getExtent();
-            tileCount = new SimpleObjectProperty[extent.getDimension()];
-            for (int i=0; i<tileCount.length; i++) {
-                (tileCount[i] = new SimpleObjectProperty<>(this, 
"tileCount")).set(extent.getSize(i));
-            }
-        }
+        final SimpleStringProperty[] tileSize;
 
         /**
-         * The callback for getting the identifier value of a row.
+         * Creates a new row for the given properties at the specified row 
index.
          */
-        static final Callback<TableColumn.CellDataFeatures<Row, String>, 
ObservableValue<String>>
-                IDENTIFIER = (cell) -> cell.getValue().identifier;
+        @SuppressWarnings("this-escape")
+        Row(final int row,
+            final String[]   identifiers,
+            final String[][] resolutions,
+            final String[][] tileCounts,
+            final String[][] tileSizes)
+        {
+            identifier = new SimpleStringProperty(this, "identifier", 
identifiers == null ? null : identifiers[row]);
+            resolution = new SimpleStringProperty[resolutions != null ? 
resolutions.length : 0];
+            tileCount  = new SimpleStringProperty[tileCounts  != null ?  
tileCounts.length : 0];
+            tileSize   = new SimpleStringProperty[tileSizes   != null ?   
tileSizes.length : 0];
+            Arrays.setAll(resolution, (i) -> new SimpleStringProperty(this, 
"resolution", resolutions[i][row]));
+            Arrays.setAll(tileCount,  (i) -> new SimpleStringProperty(this, 
"tileCount",  tileCounts [i][row]));
+            Arrays.setAll(tileSize,   (i) -> new SimpleStringProperty(this, 
"tileSize",   tileSizes  [i][row]));
+        }
 
         /**
-         * The callback for getting resolution values in the specified 
<abbr>CRS</abbr> dimension.
+         * Returns the group of columns identified by the given index.
          *
-         * @param  i  a <abbr>CRS</abbr> dimension.
-         * @return getter of resolution values in the specified 
<abbr>CRS</abbr> dimension.
+         * @param  groupIndex  1 for resolution, 2 for tile count or 3 for 
tile size. 0 is reserved for identifier.
+         * @return group of columns at the given index.
          */
-        static Callback<TableColumn.CellDataFeatures<Row, Double>, 
ObservableValue<Double>> resolution(final int i) {
-            return (cell) -> {
-                final SimpleObjectProperty<Double>[] resolution = 
cell.getValue().resolution;
-                return (i >= 0 && i < resolution.length) ? resolution[i] : 
null;
-            };
+        @SuppressWarnings("ReturnOfCollectionOrArrayField")
+        final SimpleStringProperty[] group(final int groupIndex) {
+            switch (groupIndex) {
+                case 1: return resolution;
+                case 2: return tileCount;
+                case 3: return tileSize;
+                default: throw new AssertionError(groupIndex);
+            }
         }
 
         /**
-         * The callback for getting tile count values of a row in the 
specified dimension.
+         * The callback for getting the value of the "identifier" column of a 
row.
+         */
+        static final Callback<TableColumn.CellDataFeatures<Row, String>, 
ObservableValue<String>>
+                IDENTIFIER_GETTER = (cell) -> cell.getValue().identifier;
+
+        /**
+         * The callback for getting the value of a column other than 
"identifier".
          *
-         * @param  i  a grid dimension.
-         * @return getter of tile count values in the specified grid dimension.
+         * @param  groupIndex   1 for resolution, 2 for tile count or 3 for 
tile size. 0 is reserved for identifier.
+         * @param  columnIndex  the column index in the specified group. This 
is a <abbr>CRS</abbr> or grid dimension.
+         * @return getter of values in the specified column of the specified 
group of columns.
          */
-        static Callback<TableColumn.CellDataFeatures<Row, Long>, 
ObservableValue<Long>> tileCount(final int i) {
-            return (cell) -> {
-                final SimpleObjectProperty<Long>[] tileCount = 
cell.getValue().tileCount;
-                return (i >= 0 && i < tileCount.length) ? tileCount[i] : null;
-            };
+        static Callback<TableColumn.CellDataFeatures<Row, String>, 
ObservableValue<String>>
+                getter(final int groupIndex, final int columnIndex)
+        {
+            // Index should never be out of bounds because of the way that we 
built the arrays.
+            return (cell) -> cell.getValue().group(groupIndex)[columnIndex];
         }
     }
 
@@ -226,11 +244,11 @@ public class TileMatrixSetPane extends Widget {
      */
     @SuppressWarnings("this-escape")
     public TileMatrixSetPane(final Locale locale) {
+        formatter = new TileMatrixSetFormat(locale, null);
         view = new GridPane();
         view.setPadding(SPACE_ON_TOP);
         view.setVgap(9);
         view.setHgap(12);
-        this.locale = locale;
         final Vocabulary vocabulary = Vocabulary.forLocale(locale);
         /*
          * Combox box for choosing the Tile Matrix Set.
@@ -270,12 +288,13 @@ public class TileMatrixSetPane extends Widget {
             identifier            = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Identifier));
             tileResolutionColumns = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Resolution));
             tileCountColumns      = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.TileCount));
-            identifier.setCellValueFactory(Row.IDENTIFIER);
+            tileSizeColumns       = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.TileSize));
+            identifier.setCellValueFactory(Row.IDENTIFIER_GETTER);
 
             tileMatrices = new TableView<>();
             tileMatrices.setEditable(false);
             
tileMatrices.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS);
-            tileMatrices.getColumns().setAll(List.of(identifier, 
tileResolutionColumns, tileCountColumns));
+            tileMatrices.getColumns().setAll(List.of(identifier, 
tileResolutionColumns, tileCountColumns, tileSizeColumns));
             GridPane.setHgrow(tileMatrices, Priority.ALWAYS);
             GridPane.setVgrow(tileMatrices, Priority.ALWAYS);
             GridPane.setColumnSpan(tileMatrices, 2);
@@ -286,6 +305,45 @@ public class TileMatrixSetPane extends Widget {
         
tileMatriceSets.getSelectionModel().selectedItemProperty().addListener((p,o,n) 
-> tileMatrixSetChanged(n));
     }
 
+    /**
+     * Adds or removes columns in the "resolution", "tile count" or "tile 
size" group of columns.
+     * After this method calls, the number of columns of the group will be 
equal to {@code numCols}.
+     * If that number is zero, the element is removed from the table.
+     *
+     * @param row         any row that can be used for determining {@code 
numCols}, or {@code null} if none.
+     * @param groupIndex  1 for resolution, 2 for tile count or 3 for tile 
size. 0 is reserved for identifier.
+     * @param group       group of columns identified by {@code groupIndex}.
+     * @param headerText  provider of labels for the column header. The 
function can return {@code null}.
+     */
+    private void addOrRemoveColumns(final Row row, final int groupIndex,
+                                    final TableColumn<Row, String> group,
+                                    final IntFunction<String> headerText)
+    {
+        final int numCols = (row != null) ? row.group(groupIndex).length : 0;
+        final ObservableList<TableColumn<Row, ?>> columns = group.getColumns();
+        for (int columnIndex = 0; columnIndex < numCols; columnIndex++) {
+            String header = headerText.apply(columnIndex);
+            if (header == null) {
+                header = String.valueOf(columnIndex);
+            }
+            if (columnIndex < columns.size()) {
+                columns.get(columnIndex).setText(header);
+            } else {
+                final var column = new TableColumn<Row, String>(header);
+                column.setCellFactory(AlignedTableCell.baselineRight());
+                column.setCellValueFactory(Row.getter(groupIndex, 
columnIndex));
+                columns.add(column);
+            }
+        }
+        columns.remove(numCols, columns.size());
+        final ObservableList<TableColumn<Row, ?>> table = 
tileMatrices.getColumns();
+        if (columns.isEmpty()) {
+            table.remove(group);
+        } else if (!table.contains(group)) {
+            table.add(group);
+        }
+    }
+
     /**
      * Sets the data for which to show the tiling.
      * This is a convenience method for setting {@link #contentProperty} value.
@@ -365,91 +423,57 @@ public class TileMatrixSetPane extends Widget {
         tileMatrices.getItems().clear();
         crsName.setText(null);
         if (newValue != null) {
-            BackgroundThreads.execute(new Task<TileMatrix[]>() {
-                /** The coordinate reference system. */
-                private CoordinateReferenceSystem crs;
-
-                /** Name of the Coordinate Reference System. */
-                private String crsDisplayName;
-
-                /** The grid extent of the tiling scheme, using the first row 
as a representative value. */
-                private GridExtent tilingScheme;
-
-                /** Fetches the Tile Matrices in a background thread. */
-                @Override protected TileMatrix[] call() {
-                    crs = newValue.getCoordinateReferenceSystem();
-                    if (crs != null) {
-                        crsDisplayName = IdentifiedObjects.getDisplayName(crs, 
locale);
-                    }
-                    final Collection<? extends TileMatrix> matrices = 
newValue.getTileMatrices().values();
-                    final TileMatrix first = Containers.peekFirst(matrices);
-                    if (first != null) {
-                        tilingScheme = first.getTilingScheme().getExtent();
+            BackgroundThreads.execute(new Task<Map<?,?>>() {
+                /** The number of rows. */
+                private int numRows;
+
+                /** The warning, or {@code null} if none. */
+                private Throwable warning;
+
+                /** Fetches the Tile Matrix properties in a background thread. 
*/
+                @Override protected Map<?,?> call() {
+                    final Map<String, Object> properties;
+                    synchronized (formatter) {
+                        try {
+                            properties = formatter.formatHeader(newValue);
+                            numRows = 
formatter.formatTable(newValue.getTileMatrices().values(), properties);
+                            warning = formatter.getError().orElse(null);
+                        } finally {
+                            formatter.clear();
+                        }
                     }
-                    return matrices.toArray(TileMatrix[]::new);
+                    return properties;
                 }
 
                 /** Invoked in JavaFX thread on success. */
                 @Override protected void succeeded() {
-                    crsName.setText(crsDisplayName);
-                    final TileMatrix[] matrices = getValue();
-                    final Row[] rows = new Row[matrices.length];
-                    int crsDimension = 0, gridDimension = 0;
+                    final Map<?,?> properties = getValue();
+                    crsName.setText((String) properties.get("crsName"));
+                    final var identifiers = (String[])   
properties.get("identifiers");
+                    final var resolutions = (String[][]) 
properties.get("resolutions");
+                    final var tileCounts  = (String[][]) 
properties.get("tileCounts");
+                    final var tileSizes   = (String[][]) 
properties.get("tileSizes");
+                    final var rows        = new Row[numRows];
                     for (int i=0; i<rows.length; i++) {
-                        final var row = new Row(matrices[i]);
-                        crsDimension  = Math.max(crsDimension, 
row.resolution.length);
-                        gridDimension = Math.max(gridDimension, 
row.tileCount.length);
-                        rows[i] = row;
+                        rows[i] = new Row(i, identifiers, resolutions, 
tileCounts, tileSizes);
                     }
                     tileMatrices.getItems().setAll(rows);
-                    /*
-                     * Add or remove columns for the CRS dimensions.
-                     */
-                    {
-                        final CoordinateSystem cs = (crs != null) ? 
crs.getCoordinateSystem() : null;
-                        final ObservableList<TableColumn<Row, ?>> columns = 
tileResolutionColumns.getColumns();
-                        for (int i=0; i<crsDimension; i++) {
-                            final String header = (i < cs.getDimension())
-                                    ? cs.getAxis(i).getAbbreviation() : 
String.valueOf(i);
-                            if (i < columns.size()) {
-                                columns.get(i).setText(header);
-                            } else {
-                                final var column = new TableColumn<Row, 
Double>(header);
-                                column.setCellValueFactory(Row.resolution(i));
-                                columns.add(column);
-                            }
-                        }
-                        final int size = columns.size();
-                        if (crsDimension < size) {
-                            columns.remove(crsDimension, size);
-                        }
-                    }
-                    /*
-                     * Add or remove columns for the grid dimensions.
-                     * We use the first row for getting the grid axis 
identifiers.
-                     */
-                    final ObservableList<TableColumn<Row, ?>> columns = 
tileCountColumns.getColumns();
-                    if (rows.length != 0) {
-                        for (int i=0; i<gridDimension; i++) {
-                            String header = null;
-                            if (i < tilingScheme.getDimension()) {
-                                header = 
tilingScheme.getAxisType(i).flatMap(DimensionNameType::identifier).orElse(null);
-                            }
-                            if (header == null) {
-                                header = String.valueOf(i);
-                            }
-                            if (i < columns.size()) {
-                                columns.get(i).setText(header);
-                            } else {
-                                final var column = new TableColumn<Row, 
Long>(header);
-                                column.setCellValueFactory(Row.tileCount(i));
-                                columns.add(column);
-                            }
-                        }
-                    }
-                    final int size = columns.size();
-                    if (crsDimension < size) {
-                        columns.remove(crsDimension, size);
+                    final Row first = (rows.length != 0) ? rows[0] : null;
+                    final var crs = (CoordinateReferenceSystem) 
properties.get("crs");
+                    final CoordinateSystem cs = (crs != null) ? 
crs.getCoordinateSystem() : null;
+                    addOrRemoveColumns(first, 1, tileResolutionColumns, 
(columnIndex) -> {
+                        if (cs == null || columnIndex >= cs.getDimension()) 
return null;
+                        return cs.getAxis(columnIndex).getAbbreviation();
+                    });
+                    final var gridAxes = (String[]) properties.get("gridAxes");
+                    final IntFunction<String> headerText = (columnIndex) -> {
+                        if (gridAxes == null || columnIndex >= 
gridAxes.length) return null;
+                        return gridAxes[columnIndex];
+                    };
+                    addOrRemoveColumns(first, 2, tileCountColumns, headerText);
+                    addOrRemoveColumns(first, 3, tileSizeColumns,  headerText);
+                    if (warning != null) {
+                        reportError(warning);
                     }
                 }
 
@@ -463,6 +487,8 @@ public class TileMatrixSetPane extends Widget {
 
     /**
      * Invoked when a background task failed.
+     *
+     * @todo Should write the message somewhere instead of a window popup.
      */
     private void reportError(final Throwable exception) {
         ExceptionReporter.canNotUseResource(view, exception);
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/AlignedTableCell.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/AlignedTableCell.java
new file mode 100644
index 0000000000..a21516d1dd
--- /dev/null
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/AlignedTableCell.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.internal;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.util.Callback;
+
+
+/**
+ * A factory of table cell with some alignment.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <S>  type of rows in the table.
+ * @param  <V>  type of values in the table.
+ */
+public final class AlignedTableCell<S,T> implements Callback<TableColumn<S,T>, 
TableCell<S,T>> {
+    /**
+     * A factory of cells that are vertically centered.
+     */
+    private static final AlignedTableCell<?,?> CENTER_LEFT = new 
AlignedTableCell<>(Pos.CENTER_LEFT);
+
+    /**
+     * A factory of cells that are right-aligned.
+     */
+    private static final AlignedTableCell<?,?> BASELINE_RIGHT = new 
AlignedTableCell<>(Pos.BASELINE_RIGHT);
+
+    /**
+     * The desired alignment of text in the field.
+     */
+    private final Pos alignment;
+
+    /**
+     * Creates a new factory.
+     *
+     * @param  alignment  the desired alignment of text in the field
+     */
+    private AlignedTableCell(final Pos alignment) {
+        this.alignment = alignment;
+    }
+
+    /**
+     * Creates a new cell.
+     *
+     * @param  column  the column for which to create a cell.
+     * @return cell factory for the given column.
+     */
+    @Override
+    public TableCell<S,T> call(final TableColumn<S,T> column) {
+        @SuppressWarnings("unchecked")
+        final var cell = (TableCell<S,T>) 
TableColumn.DEFAULT_CELL_FACTORY.call(column);
+        cell.setAlignment(alignment);
+        return cell;
+    }
+
+    /**
+     * Returns a factory of cells that are vertically centered.
+     */
+    @SuppressWarnings("unchecked")
+    public static <S,T> AlignedTableCell<S,T> centerLeft() {
+        return (AlignedTableCell<S,T>) CENTER_LEFT;
+    }
+
+    /**
+     * Returns a factory of cells that are right-aligned.
+     */
+    @SuppressWarnings("unchecked")
+    public static <S,T> AlignedTableCell<S,T> baselineRight() {
+        return (AlignedTableCell<S,T>) BASELINE_RIGHT;
+    }
+}

Reply via email to