You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/10/27 16:40:10 UTC

[sis] branch geoapi-4.0 updated (93542a4 -> 23d2354)

This is an automated email from the ASF dual-hosted git repository.

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


    from 93542a4  Apply GDAL "sparse files" convention. It requires relaxing `TiledGridCoverage` tile types from `WritableRaster` to `Raster`.
     new eae7522  Remove a background thread that does not seem to be necessary anymore.
     new d517cda  Load native metadata in a background thread separated from standard metadata. This allows loading those metadata only if the "native metadata" tab is selected.
     new 26b4daa  Provide an extension point in `DefaultTreeTable` for initializing the tree only when the root node is first requested.
     new 23d2354  Provides a view of GeoTIFF native metadata (TIFF tags and GeoKeys).

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sis/gui/dataset/ResourceExplorer.java   | 227 ++++++++-------
 .../org/apache/sis/gui/dataset/package-info.java   |   6 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |  28 +-
 .../org/apache/sis/gui/metadata/MetadataTree.java  |   7 +-
 .../org/apache/sis/gui/metadata/package-info.java  |   2 +-
 .../apache/sis/internal/gui/BackgroundThreads.java |   7 -
 .../sis/util/collection/DefaultTreeTable.java      |  20 +-
 .../apache/sis/util/collection/package-info.java   |   2 +-
 .../apache/sis/internal/geotiff/Compression.java   |  34 ++-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java | 208 ++------------
 .../apache/sis/storage/geotiff/GeoKeysLoader.java  | 308 +++++++++++++++++++++
 .../org/apache/sis/storage/geotiff/GeoTIFF.java    |   1 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  44 ++-
 .../sis/storage/geotiff/GridGeometryBuilder.java   |  22 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   9 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java | 249 +++++++++++++++++
 .../org/apache/sis/storage/geotiff/Reader.java     |   4 +-
 .../java/org/apache/sis/storage/geotiff/Type.java  |  60 +++-
 .../apache/sis/storage/geotiff/package-info.java   |   2 +-
 19 files changed, 855 insertions(+), 385 deletions(-)
 create mode 100644 storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoKeysLoader.java
 create mode 100644 storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java

[sis] 01/04: Remove a background thread that does not seem to be necessary anymore.

Posted by de...@apache.org.
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 eae7522921b6c7a54a9b74e208fc7d9cc55d95f5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Oct 27 11:24:31 2021 +0200

    Remove a background thread that does not seem to be necessary anymore.
---
 .../apache/sis/gui/dataset/ResourceExplorer.java   | 73 +---------------------
 .../org/apache/sis/gui/dataset/package-info.java   |  6 +-
 .../apache/sis/internal/gui/BackgroundThreads.java |  7 ---
 3 files changed, 5 insertions(+), 81 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index b19331f..aed5ecf 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -49,7 +49,6 @@ import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.BackgroundThreads;
-import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.LogHandler;
 
 
@@ -58,7 +57,7 @@ import org.apache.sis.internal.gui.LogHandler;
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -133,12 +132,6 @@ public class ResourceExplorer extends WindowManager {
     private double dividerPosition;
 
     /**
-     * Used for building {@link #viewTab} and {@link #tableTab} when first needed.
-     * This is {@code null} the rest of the time.
-     */
-    private transient DataTabBuilder builder;
-
-    /**
      * Creates a new panel for exploring resources.
      */
     public ResourceExplorer() {
@@ -197,58 +190,6 @@ public class ResourceExplorer extends WindowManager {
          */
         viewTab .selectedProperty().addListener((p,o,n) -> dataTabShown(n, true));
         tableTab.selectedProperty().addListener((p,o,n) -> dataTabShown(n, false));
-        /*
-         * Optional execution in advance of a potentially slow operation. If `PRELOAD` is `false`,
-         * then the data tabs will be initialized only the first time that one of those tabs is
-         * visible at the same time that a resource is selected in the resources explorer.
-         */
-        if (BackgroundThreads.PRELOAD) {
-            builder = new DataTabBuilder(null);
-            BackgroundThreads.execute(builder);
-        }
-    }
-
-    /**
-     * A background task for building the content of {@link #viewTab} and {@link #tableTab} when first needed.
-     * This task does not load data, it is only for building the GUI. This operation is longer for those tabs
-     * when built for the first time.
-     */
-    private final class DataTabBuilder extends Task<CoverageExplorer> {
-        /**
-         * The resource to show after construction is completed, or {@code null} if none.
-         */
-        volatile Resource resource;
-
-        /**
-         * Creates a new data tabs builder. The given resource will be shown after
-         * the tabs are ready, unless {@link #resource} is modified after construction.
-         */
-        DataTabBuilder(final Resource resource) {
-            this.resource = resource;
-        }
-
-        /** Builds the tabs GUI components in a background thread. */
-        @Override protected CoverageExplorer call() {
-            return new CoverageExplorer();
-        }
-
-        /** Shows the resource after the tabs GUI are built. */
-        @Override protected void succeeded() {
-            builder  = null;
-            coverage = getValue();
-            updateDataTab(resource, true);
-        }
-
-        /** Invoked if the tabs can not be built. */
-        @Override protected void failed() {
-            builder = null;
-            ExceptionReporter.show(getView(), this);
-        }
-
-        /** Should never happen, but defined as a safety. */
-        @Override protected void cancelled() {
-            builder = null;
-        }
     }
 
     /**
@@ -369,14 +310,6 @@ public class ResourceExplorer extends WindowManager {
      *                   if the given resource is an aggregate.
      */
     private void updateDataTab(final Resource resource, boolean fallback) {
-        /*
-         * If tabs are being built in a background thread, wait for construction to finish.
-         * The builder will callback this `updateDataTab(resource, true)` method when ready.
-         */
-        if (builder != null) {
-            builder.resource = resource;
-            return;
-        }
         Region       image = null;
         Region       table = null;
         FeatureSet   data  = null;
@@ -384,9 +317,7 @@ public class ResourceExplorer extends WindowManager {
         CoverageExplorer.View type = null;
         if (resource instanceof GridCoverageResource) {
             if (coverage == null) {
-                builder = new DataTabBuilder(resource);
-                BackgroundThreads.execute(builder);
-                return;
+                coverage = new CoverageExplorer();
             }
             grid  = new ImageRequest((GridCoverageResource) resource, null, null);
             image = coverage.getDataView(CoverageExplorer.View.IMAGE);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
index 318155f..67ae30e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/package-info.java
@@ -18,13 +18,13 @@
 /**
  * Widgets about data store resources and their metadata.
  * Those widgets can show a hierarchical collection of {@link org.apache.sis.storage.Resource}s in a tree,
- * and show their content in other panel when a resource is selected. The resources can optionally be loaded
- * from a file in background thread.
+ * and show their content in other panel when a resource is selected.
+ * The resources can optionally be loaded from a file in background thread.
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
index c2e9316..1251829 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
@@ -53,13 +53,6 @@ import org.apache.sis.util.logging.Logging;
 @SuppressWarnings("serial")                         // Not intended to be serialized.
 public final class BackgroundThreads extends AtomicInteger implements ThreadFactory {
     /**
-     * Whether to allow the application to prepare in a background thread some GUI component
-     * before they are actually needed. It allows faster user experience, at the cost of CPU
-     * and memory consumption that may not be needed.
-     */
-    public static final boolean PRELOAD = true;
-
-    /**
      * The executor for background tasks. This is actually an {@link ExecutorService} instance,
      * but only the {@link Executor} method should be used according JavaFX documentation.
      */

[sis] 03/04: Provide an extension point in `DefaultTreeTable` for initializing the tree only when the root node is first requested.

Posted by de...@apache.org.
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 26b4daa6a56c6768710b74d086235548e0298d7e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Oct 27 15:17:17 2021 +0200

    Provide an extension point in `DefaultTreeTable` for initializing the tree only when the root node is first requested.
---
 .../apache/sis/util/collection/DefaultTreeTable.java | 20 +++++++++++++++++---
 .../org/apache/sis/util/collection/package-info.java |  2 +-
 2 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
index ae0a2c8..ff7f48e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
@@ -64,7 +64,7 @@ import static org.apache.sis.util.collection.Containers.hashMapCapacity;
  * implementation provided in the {@link Node} inner class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.3
+ * @version 1.2
  *
  * @see Node
  * @see TableColumn
@@ -208,6 +208,7 @@ public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
     public TreeTable.Node getRoot() {
         if (root == null) {
             root = new Node(this);
+            initialize(root);
         }
         return root;
     }
@@ -232,6 +233,19 @@ public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
     }
 
     /**
+     * Invoked when {@link #getRoot()} is invoked for the first time and no root had been specified to the constructor.
+     * The {@code root} argument is a newly created empty node to be returned by {@link #getRoot()}.
+     * The default implementation does nothing.
+     * Subclasses can override for lazy initialization of tree table content.
+     *
+     * @param  root  a newly created tree table root.
+     *
+     * @since 1.2
+     */
+    protected void initialize(final TreeTable.Node root) {
+    }
+
+    /**
      * Returns a clone of this table. This method clones the {@linkplain #getRoot() root} node.
      * If the root is an instance of {@link Node}, then cloning the root will recursively clone
      * all its {@linkplain Node#getChildren() children}.
@@ -268,7 +282,7 @@ public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
         if (other != null && other.getClass() == getClass()) {
             final DefaultTreeTable that = (DefaultTreeTable) other;
             return columnIndices.equals(that.columnIndices) &&
-                    Objects.equals(root, that.root);
+                    Objects.equals(getRoot(), that.getRoot());
         }
         return false;
     }
@@ -281,7 +295,7 @@ public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
      */
     @Override
     public int hashCode() {
-        return (columnIndices.hashCode() + 31*Objects.hashCode(root)) ^ (int) serialVersionUID;
+        return (columnIndices.hashCode() + 31*Objects.hashCode(getRoot())) ^ (int) serialVersionUID;
     }
 
     /**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/package-info.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/package-info.java
index 9c844bb..56d7425 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/package-info.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/package-info.java
@@ -51,7 +51,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.3
  * @module
  */

[sis] 02/04: Load native metadata in a background thread separated from standard metadata. This allows loading those metadata only if the "native metadata" tab is selected.

Posted by de...@apache.org.
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 d517cda884a5d80fb43b96ac163fa2e4f9bdf28a
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Oct 27 15:16:08 2021 +0200

    Load native metadata in a background thread separated from standard metadata.
    This allows loading those metadata only if the "native metadata" tab is selected.
---
 .../apache/sis/gui/dataset/ResourceExplorer.java   | 156 +++++++++++++++++----
 .../apache/sis/gui/metadata/MetadataSummary.java   |  28 +---
 .../org/apache/sis/gui/metadata/MetadataTree.java  |   7 +-
 .../org/apache/sis/gui/metadata/package-info.java  |   2 +-
 4 files changed, 132 insertions(+), 61 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
index aed5ecf..d0936a4 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/ResourceExplorer.java
@@ -20,6 +20,7 @@ import java.util.Objects;
 import java.util.Collection;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import javafx.beans.binding.BooleanBinding;
 import javafx.beans.property.ReadOnlyProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import javafx.collections.ListChangeListener;
@@ -39,16 +40,19 @@ import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataSet;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.gui.metadata.MetadataSummary;
 import org.apache.sis.gui.metadata.MetadataTree;
 import org.apache.sis.gui.metadata.StandardMetadataTree;
 import org.apache.sis.gui.coverage.ImageRequest;
 import org.apache.sis.gui.coverage.CoverageExplorer;
-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.internal.gui.Resources;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.LogHandler;
 
 
@@ -76,10 +80,28 @@ public class ResourceExplorer extends WindowManager {
 
     /**
      * The widget showing metadata about a selected resource.
+     * Its content will be updated only when the tab is visible.
      */
     private final MetadataSummary metadata;
 
     /**
+     * The widget showing native metadata about a selected resource.
+     * Its content will be updated only when the tab is visible.
+     */
+    private final MetadataTree nativeMetadata;
+
+    /**
+     * The tab containing {@link #nativeMetadata}.
+     * The table title will change depending on the selected resource.
+     */
+    private final Tab nativeMetadataTab;
+
+    /**
+     * Default label for {@link #nativeMetadataTab} when no resource is selected.
+     */
+    private final String defaultNativeTabLabel;
+
+    /**
      * The gridded data as an image or as a table, created when first needed.
      */
     private CoverageExplorer coverage;
@@ -126,6 +148,11 @@ public class ResourceExplorer extends WindowManager {
     private boolean isDataTabSet;
 
     /**
+     * Whether one of the standard metadata tab (either "summary" or "metadata") is selected.
+     */
+    private final BooleanBinding metadataShown;
+
+    /**
      * Last divider position as a fraction between 0 and 1, or {@code NaN} if undefined.
      * This is used for keeping the position constant when adding and removing controls.
      */
@@ -142,42 +169,48 @@ public class ResourceExplorer extends WindowManager {
         resources.getSelectionModel().getSelectedItems().addListener(this::onResourceSelected);
         resources.setPrefWidth(400);
         selectedResource = new ReadOnlyObjectWrapper<>(this, "selectedResource");
+        final Vocabulary vocabulary = Vocabulary.getResources(resources.locale);
+        /*
+         * "Summary" tab showing a summary of resource metadata.
+         */
         metadata = new MetadataSummary();
+        final Tab summaryTab = new Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView());
         /*
-         * Build the tabs.
+         * "Visual" tab showing the raster data as an image.
+         *
+         * TODO: add contextual menu for creating a window showing directly the visual.
          */
-        final Vocabulary vocabulary = Vocabulary.getResources(resources.locale);
         viewTab = new Tab(vocabulary.getString(Vocabulary.Keys.Visual));
-        // TODO: add contextual menu for window showing directly the visual.
-
+        /*
+         * "Data" tab showing raster data as a table.
+         */
         tableTab = new Tab(vocabulary.getString(Vocabulary.Keys.Data));
         tableTab.setContextMenu(new ContextMenu(SelectedData.setTabularView(createNewWindowMenu())));
+        /*
+         * "Metadata" tab showing ISO 19115 metadata as a tree.
+         */
+        final Tab metadataTab = new Tab(vocabulary.getString(Vocabulary.Keys.Metadata), new StandardMetadataTree(metadata));
+        /*
+         * "Native metadata" tab showing metadata in their "raw" form (specific to the format).
+         */
+        nativeMetadata = new MetadataTree(metadata);
+        defaultNativeTabLabel = vocabulary.getString(Vocabulary.Keys.Format);
+        nativeMetadataTab = new Tab(defaultNativeTabLabel, nativeMetadata);
+        nativeMetadataTab.setDisable(true);
+        /*
+         * "Logging" tab showing log records specific to the selected resource
+         * (as opposed to the application menu showing all loggings regardless their source).
+         */
         final LogViewer logging = new LogViewer(vocabulary);
         logging.source.bind(selectedResource);
-
-        final String nativeTabText = vocabulary.getString(Vocabulary.Keys.Format);
-        final MetadataTree nativeMetadata = new MetadataTree(metadata);
-        final Tab nativeTab = new Tab(nativeTabText, nativeMetadata);
-        nativeTab.setDisable(true);
-        nativeMetadata.contentProperty.addListener((p,o,n) -> {
-            nativeTab.setDisable(n == null);
-            Object label = (n != null) ? n.getRoot().getValue(TableColumn.NAME) : null;
-            nativeTab.setText(Objects.toString(label, nativeTabText));
-        });
-
         final Tab loggingTab = new Tab(vocabulary.getString(Vocabulary.Keys.Logs), logging.getView());
         loggingTab.disableProperty().bind(logging.isEmptyProperty());
-
-        final TabPane tabs = new TabPane(
-            new Tab(vocabulary.getString(Vocabulary.Keys.Summary),  metadata.getView()), viewTab, tableTab,
-            new Tab(vocabulary.getString(Vocabulary.Keys.Metadata), new StandardMetadataTree(metadata)),
-            nativeTab, loggingTab);
-
-        tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
-        tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
         /*
          * Build the main pane which put everything together.
          */
+        final TabPane tabs = new TabPane(summaryTab, viewTab, tableTab, metadataTab, nativeMetadataTab, loggingTab);
+        tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
+        tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
         controls = new SplitPane(resources);
         controls.setOrientation(Orientation.VERTICAL);
         content = new SplitPane(controls, tabs);
@@ -187,9 +220,22 @@ public class ResourceExplorer extends WindowManager {
         SplitPane.setResizableWithParent(tabs, Boolean.TRUE);
         /*
          * Register listeners last, for making sure we don't have undesired event.
+         * Those listeners trig loading of various objects (data, standard metadata,
+         * native metadata) when the corresponding tab become visible.
          */
         viewTab .selectedProperty().addListener((p,o,n) -> dataTabShown(n, true));
         tableTab.selectedProperty().addListener((p,o,n) -> dataTabShown(n, false));
+        metadataShown = summaryTab.selectedProperty().or(metadataTab.selectedProperty());
+        metadataShown.addListener((p,o,n) -> {
+            if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) {
+                metadata.setMetadata(getSelectedResource());
+            }
+        });
+        nativeMetadataTab.selectedProperty().addListener((p,o,n) -> {
+            if (Boolean.FALSE.equals(o) && Boolean.TRUE.equals(n)) {
+                loadNativeMetadata();
+            }
+        });
     }
 
     /**
@@ -279,20 +325,72 @@ public class ResourceExplorer extends WindowManager {
                 if (resource != null) break;
             }
         }
+        /*
+         * Fetch metadata immediately if one of the two ISO 19115 metadata tabs is selected.
+         * Otherwise metadata will be fetched when one of those tabs will become selected
+         * (listener registered in the constructor). A similar policy is applied for data.
+         */
         selectedResource.set(resource);
-        metadata.setMetadata(resource);
-        isDataTabSet = isDataTabSelected();
+        metadata.setMetadata(metadataShown.get() ? resource : null);
+        isDataTabSet = viewTab.isSelected() || tableTab.isSelected();
         updateDataTab(isDataTabSet ? resource : null, true);
         if (!isDataTabSet) {
             setNewWindowDisabled(!(resource instanceof GridCoverageResource || resource instanceof FeatureSet));
         }
+        /*
+         * Update the label is disabled state of the native metadata tab. We do not have a reliable way
+         * to know if metadata are present without trying to fetch them, so current implementation only
+         * checks if the data store implementation override the `getNativeMetadata()` method.
+         */
+        String  label    = null;
+        boolean disabled = true;
+        if (resource instanceof DataStore) {
+            final DataStore store = (DataStore) resource;
+            final DataStoreProvider provider = store.getProvider();
+            if (provider != null) {
+                label = provider.getShortName();
+            }
+            try {
+                disabled = resource.getClass().getMethod("getNativeMetadata").getDeclaringClass() == DataStore.class;
+            } catch (NoSuchMethodException e) {
+                // Should never happen.
+            }
+        }
+        nativeMetadataTab.setText(Objects.toString(label, defaultNativeTabLabel));
+        nativeMetadataTab.setDisable(disabled);
+        nativeMetadata.setPlaceholder(null);
+        nativeMetadata.setContent(null);
+        if (nativeMetadataTab.isSelected()) {
+            loadNativeMetadata();
+        }
     }
 
     /**
-     * Returns whether the currently selected tab is {@link #viewTab} or {@link #tableTab}.
+     * Loads native metadata in a background thread and shows them in the "native metadata" tab.
      */
-    private boolean isDataTabSelected() {
-        return viewTab.isSelected() || tableTab.isSelected();
+    private final void loadNativeMetadata() {
+        final Resource resource = getSelectedResource();
+        if (resource instanceof DataStore) {
+            final DataStore store = (DataStore) resource;
+            BackgroundThreads.execute(new Task<TreeTable>() {
+                /** Invoked in a background thread for fetching metadata. */
+                @Override protected TreeTable call() throws DataStoreException {
+                    return store.getNativeMetadata().orElse(null);
+                }
+
+                /** Shows the result in JavaFX thread. */
+                @Override protected void succeeded() {
+                    if (resource == getSelectedResource()) {
+                        nativeMetadata.setContent(getValue());
+                    }
+                }
+
+                /** Invoked in JavaFX thread if metadata loading failed. */
+                @Override protected void failed() {
+                    nativeMetadata.setPlaceholder(new ExceptionReporter(getException()).getView());
+                }
+            });
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
index 8bc8a0d..dee04f1 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataSummary.java
@@ -20,7 +20,6 @@ import java.util.Locale;
 import java.text.DateFormat;
 import java.text.NumberFormat;
 import java.util.Collection;
-import java.util.ArrayList;
 import java.util.StringJoiner;
 import javafx.application.Platform;
 import javafx.beans.DefaultProperty;
@@ -42,13 +41,11 @@ import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.internal.util.Strings;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.gui.Widget;
 
@@ -58,7 +55,7 @@ import org.apache.sis.gui.Widget;
  *
  * @author  Smaniotto Enzo (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -133,13 +130,6 @@ public class MetadataSummary extends Widget {
     private final TitledPane[] information;
 
     /**
-     * The listeners to notify about native metadata. We define those listeners in this {@link MetadataSummary}
-     * for taking advantage of the background loading mechanism. We do not provide public API for such listeners
-     * because a future version may want to provide those listeners in a more appropriate (not yet defined) class.
-     */
-    final ArrayList<MetadataTree> nativeMetadataViews;
-
-    /**
      * Creates an initially empty metadata overview.
      */
     public MetadataSummary() {
@@ -151,7 +141,6 @@ public class MetadataSummary extends Widget {
         };
         content = new ScrollPane(new VBox());
         content.setFitToWidth(true);
-        nativeMetadataViews = new ArrayList<>();
         metadataProperty = new SimpleObjectProperty<>(this, "metadata");
         metadataProperty.addListener(MetadataSummary::applyChange);
     }
@@ -230,9 +219,6 @@ public class MetadataSummary extends Widget {
         /** The resource from which to load metadata. */
         private final Resource resource;
 
-        /** The native metadata, or {@code null} if none or not requested. */
-        TreeTable nativeMetadata;
-
         /** Creates a new metadata getter. */
         Getter(final Resource resource) {
             this.resource = resource;
@@ -240,9 +226,6 @@ public class MetadataSummary extends Widget {
 
         /** Invoked in a background thread for fetching metadata. */
         @Override protected Metadata call() throws DataStoreException {
-            if (resource instanceof DataStore && !nativeMetadataViews.isEmpty()) {
-                nativeMetadata = ((DataStore) resource).getNativeMetadata().orElse(null);
-            }
             return resource.getMetadata();
         }
 
@@ -328,15 +311,6 @@ public class MetadataSummary extends Widget {
         s.getter = null;                // In case this method is invoked before `Getter` completed.
         s.error  = null;
         if (metadata != oldValue) {
-            /*
-             * The native metadata are handled in a special way by `setMetadata(Resource)`.
-             * Since we have no public API for setting native metadata in `MetadataSummary`,
-             * we set the value to null if this method is not invoked from `Getter.succeeded()`.
-             */
-            final TreeTable nativeMetadata = (getter != null && getter.isDone()) ? getter.nativeMetadata : null;
-            for (final MetadataTree view : s.nativeMetadataViews) {
-                view.setContent(nativeMetadata);
-            }
             final ObservableList<Node> children = s.getChildren();
             if (!children.isEmpty() && !(children.get(0) instanceof Section)) {
                 children.clear();       // If we were previously showing an error, clear all.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
index a1a725b..3d322fa 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/MetadataTree.java
@@ -68,7 +68,7 @@ import org.apache.sis.util.logging.Logging;
  *
  * @author  Siddhesh Rane (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -185,9 +185,6 @@ check:      if (data != null) {
         contentProperty.addListener(MetadataTree::applyChange);
         if (!standard) {
             setShowRoot(false);
-            if (controller != null) {
-                controller.nativeMetadataViews.add(this);
-            }
         }
     }
 
@@ -231,6 +228,8 @@ check:      if (data != null) {
 
     /**
      * Invoked when {@link #contentProperty} value changed.
+     * This method invokes {@link TreeTable#getRoot()} and
+     * wraps the value as the root node of this control.
      *
      * @param  property  the property which has been modified.
      * @param  oldValue  the old tree table.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/package-info.java
index 413a219..3d94a52 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/metadata/package-info.java
@@ -23,7 +23,7 @@
  * @author  Smaniotto Enzo (GSoC)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */

[sis] 04/04: Provides a view of GeoTIFF native metadata (TIFF tags and GeoKeys).

Posted by de...@apache.org.
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 23d23543e85407e8dc98baa7692caba04b95a6fb
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Oct 27 18:39:41 2021 +0200

    Provides a view of GeoTIFF native metadata (TIFF tags and GeoKeys).
---
 .../apache/sis/internal/geotiff/Compression.java   |  34 ++-
 .../org/apache/sis/storage/geotiff/CRSBuilder.java | 208 ++------------
 .../apache/sis/storage/geotiff/GeoKeysLoader.java  | 308 +++++++++++++++++++++
 .../org/apache/sis/storage/geotiff/GeoTIFF.java    |   1 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  44 ++-
 .../sis/storage/geotiff/GridGeometryBuilder.java   |  22 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   9 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java | 249 +++++++++++++++++
 .../org/apache/sis/storage/geotiff/Reader.java     |   4 +-
 .../java/org/apache/sis/storage/geotiff/Type.java  |  60 +++-
 .../apache/sis/storage/geotiff/package-info.java   |   2 +-
 11 files changed, 701 insertions(+), 240 deletions(-)

diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Compression.java b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Compression.java
index 6c6e86f..a803031 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Compression.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/internal/geotiff/Compression.java
@@ -30,7 +30,7 @@ package org.apache.sis.internal.geotiff;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.8
  * @module
  */
@@ -137,24 +137,22 @@ public enum Compression {
      * @param  code  the TIFF code for which to get a compression enumeration value.
      * @return enumeration value for the given code, or {@link #UNKNOWN} if none.
      */
-    public static Compression valueOf(final long code) {
-        if ((code & ~0xFFFF) == 0) {                // Should be a short according TIFF specification.
-            switch ((int) code) {
-                case 1:     return NONE;
-                case 2:     return CCITTRLE;
-                case 5:     return LZW;
-                case 6:     // "old-style" JPEG, later overriden in Technical Notes 2.
-                case 7:     return JPEG;
-                case 8:
-                case 32946: return DEFLATE;
-                case 32773: return PACKBITS;
-                default: {
-                    // Fallback for uncommon formats.
-                    for (final Compression c : values()) {
-                        if (c.code == code) return c;
-                    }
-                    break;
+    public static Compression valueOf(final int code) {
+        switch (code) {
+            case 1:     return NONE;
+            case 2:     return CCITTRLE;
+            case 5:     return LZW;
+            case 6:     // "old-style" JPEG, later overriden in Technical Notes 2.
+            case 7:     return JPEG;
+            case 8:
+            case 32946: return DEFLATE;
+            case 32773: return PACKBITS;
+            default: {
+                // Fallback for uncommon formats.
+                for (final Compression c : values()) {
+                    if (c.code == code) return c;
                 }
+                break;
             }
         }
         return UNKNOWN;
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 d3e2aa0..87efd7b 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
@@ -83,66 +83,20 @@ import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 
 /**
  * Helper class for building a {@link CoordinateReferenceSystem} from information found in TIFF tags.
- * A {@code CRSBuilder} receives as inputs the values of the following TIFF tags:
- *
- * <ul>
- *   <li>{@link Tags#GeoKeyDirectory} — array of unsigned {@code short} values grouped into blocks of 4.</li>
- *   <li>{@link Tags#GeoDoubleParams} — array of {@double} values referenced by {@code GeoKeyDirectory} elements.</li>
- *   <li>{@link Tags#GeoAsciiParams}  — array of characters referenced by {@code GeoKeyDirectory} elements.</li>
- * </ul>
- *
- * For example, consider the following values for the above-cited tags:
- *
- * <table class="sis">
- *   <caption>GeoKeyDirectory(34735) values</caption>
- *   <tr><td>    1 </td><td>     1 </td><td>  2 </td><td>     6 </td></tr>
- *   <tr><td> 1024 </td><td>     0 </td><td>  1 </td><td>     2 </td></tr>
- *   <tr><td> 1026 </td><td> 34737 </td><td>  0 </td><td>    12 </td></tr>
- *   <tr><td> 2048 </td><td>     0 </td><td>  1 </td><td> 32767 </td></tr>
- *   <tr><td> 2049 </td><td> 34737 </td><td> 14 </td><td>    12 </td></tr>
- *   <tr><td> 2050 </td><td>     0 </td><td>  1 </td><td>     6 </td></tr>
- *   <tr><td> 2051 </td><td> 34736 </td><td>  1 </td><td>     0 </td></tr>
- * </table>
- *
- * {@preformattext
- *   GeoDoubleParams(34736) = {1.5}
- *   GeoAsciiParams(34737) = "Custom File|My Geographic|"
- * }
- *
- * <p>The first number in the {@code GeoKeyDirectory} table indicates that this is a version 1 GeoTIFF GeoKey directory.
- * This version will only change if the key structure is changed. The other numbers on the first line said that the file
- * uses revision 1.2 of the set of keys and that there is 6 key values.</p>
- *
- * <p>The next line indicates that the first key (1024 = {@code ModelType}) has the value 2 (Geographic),
- * explicitly placed in the entry list since the TIFF tag location is 0.
- * The next line indicates that the key 1026 ({@code Citation}) is listed in the {@code GeoAsciiParams(34737)} array,
- * starting at offset 0 (the first in array), and running for 12 bytes and so has the value "Custom File".
- * The "|" character is converted to a null delimiter at the end in C/C++ libraries.</p>
- *
- * <p>Going further down the list, the key 2051 ({@code GeogLinearUnitSize}) is located in {@code GeoDoubleParams(34736)}
- * at offset 0 and has the value 1.5; the value of key 2049 ({@code GeogCitation}) is "My Geographic".</p>
+ * GeoKeys are loaded by {@link GeoKeysLoader} and consumed by this class.
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see GeoKeys
+ * @see GeoKeysLoader
  *
  * @since 0.8
  * @module
  */
 final class CRSBuilder extends ReferencingFactoryContainer {
     /**
-     * Number of {@code short} values in each GeoKey entry.
-     */
-    private static final int ENTRY_LENGTH = 4;
-
-    /**
-     * The character used as a separator in {@link String} multi-values.
-     */
-    private static final char SEPARATOR = '|';
-
-    /**
      * Index where to store the name of the geodetic CRS, the datum, the ellipsoid and the prime meridian.
      * The GeoTIFF specification has only one key, {@link GeoKeys#GeogCitation}, for the geographic CRS and
      * its components. But some GeoTIFF files encode the names of all components in the value associated to
@@ -241,8 +195,9 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      * @param  args  arguments for the log message.
      *
      * @see Resources
+     * @see GeoKeysLoader#warning(short, Object...)
      */
-    private void warning(final short key, final Object... args) {
+    final void warning(final short key, final Object... args) {
         final LogRecord r = reader.resources().getLogRecord(Level.WARNING, key, args);
         reader.store.warning(r);
     }
@@ -400,8 +355,10 @@ final class CRSBuilder extends ReferencingFactoryContainer {
     /**
      * Reports a warning about missing value for the given key. The key name is opportunistically returned for
      * building the {@link NoSuchElementException} message, but it is not the main purpose of this method.
+     *
+     * @see GeoKeysLoader#missingValue(short)
      */
-    private String missingValue(final short key) {
+    final String missingValue(final short key) {
         final String name = GeoKeys.name(key);
         warning(Resources.Keys.MissingGeoValue_1, name);
         return name;
@@ -490,9 +447,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      * The {@link #description} and {@link #cellGeometry} fields are set as a side-effect.
      * A warning is emitted if any GeoTIFF tags were ignored.
      *
-     * @param  keyDirectory       the GeoTIFF keys to be associated to values. Can not be null.
-     * @param  numericParameters  a vector of {@code double} parameters, or {@code null} if none.
-     * @param  asciiParameters    the sequence of characters from which to build strings, or {@code null} if none.
+     * @param  source  the {@code keyDirectory}, {@code numericParameters} and {@code asciiParameters} tags.
      * @return the coordinate reference system created from the given GeoTIFF keys, or {@code null} if undefined.
      *
      * @throws NoSuchElementException if a mandatory value is missing.
@@ -501,144 +456,19 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      * @throws FactoryException if an error occurred during objects creation with the factories.
      */
     @SuppressWarnings("null")
-    public CoordinateReferenceSystem build(final Vector keyDirectory, final Vector numericParameters, final String asciiParameters)
-            throws FactoryException
-    {
-        final int numberOfKeys;
-        final int directoryLength = keyDirectory.size();
-        if (directoryLength >= ENTRY_LENGTH) {
-            final int version = keyDirectory.intValue(0);
-            if (version != 1) {
-                warning(Resources.Keys.UnsupportedGeoKeyDirectory_1, version);
+    public CoordinateReferenceSystem build(final GeoKeysLoader source) throws FactoryException {
+        try {
+            source.logger = this;
+            if (!source.load(geoKeys)) {
                 return null;
             }
-            majorRevision = keyDirectory.shortValue(1);
-            minorRevision = keyDirectory.shortValue(2);
-            numberOfKeys  = keyDirectory.intValue(3);
-        } else {
-            numberOfKeys = 0;
-        }
-        /*
-         * The key directory may be longer than needed for the amount of keys, but not shorter.
-         * If shorter, report a warning and stop the parsing since we have no way to know if the
-         * missing information were essentiel or not.
-         *
-         *     (number of key + head) * 4    ---    1 entry = 4 short values.
-         */
-        final int expectedLength = (numberOfKeys + 1) * ENTRY_LENGTH;
-        if (directoryLength < expectedLength) {
-            warning(Resources.Keys.ListTooShort_3, "GeoKeyDirectory", expectedLength, directoryLength);
-            return null;
-        }
-        final int numberOfDoubles = (numericParameters != null) ? numericParameters.size() : 0;
-        final int numberOfChars   =   (asciiParameters != null) ? asciiParameters.length() : 0;
-        /*
-         * Now iterate over all GeoKey values. The values are copied in a HashMap for convenience,
-         * because the CRS creation may use them out of order.
-         */
-        for (int i=1; i <= numberOfKeys; i++) {
-            final int p = i * ENTRY_LENGTH;
-            final short key       = keyDirectory.shortValue(p);
-            final int tagLocation = keyDirectory.intValue(p+1);
-            final int count       = keyDirectory.intValue(p+2);
-            final int valueOffset = keyDirectory.intValue(p+3);
-            if (valueOffset < 0 || count < 0) {
-                missingValue(key);
-                continue;
-            }
-            final Object value;
-            switch (tagLocation) {
-                /*
-                 * tagLocation == 0 means that 'valueOffset' actually contains the value,
-                 * thus avoiding the need to allocate a separated storage location for it.
-                 * The count should be 1.
-                 */
-                case 0: {
-                    switch (count) {
-                        case 0:  continue;
-                        case 1:  break;          // Expected value.
-                        default: warning(Resources.Keys.UnexpectedListOfValues_2, GeoKeys.name(key), count); break;
-                    }
-                    value = valueOffset;
-                    break;
-                }
-                /*
-                 * Values of type 'short' are stored in the same vector than the key directory;
-                 * the specification does not allocate a separated vector for them. We use the
-                 * 'int' type if needed for allowing storage of unsigned short values.
-                 */
-                case Tags.GeoKeyDirectory & 0xFFFF: {
-                    if (valueOffset + count > keyDirectory.size()) {
-                        missingValue(key);
-                        continue;
-                    }
-                    switch (count) {
-                        case 0:  continue;
-                        case 1:  value = keyDirectory.get(valueOffset); break;
-                        default: final int[] array = new int[count];
-                                 for (int j=0; j<count; j++) {
-                                     array[j] = keyDirectory.intValue(valueOffset + j);
-                                 }
-                                 value = array;
-                                 break;
-                    }
-                    break;
-                }
-                /*
-                 * Values of type 'double' are read from a separated vector, 'numericParameters'.
-                 * Result is stored in a Double wrapper or in an array of type 'double[]'.
-                 */
-                case Tags.GeoDoubleParams & 0xFFFF: {
-                    if (valueOffset + count > numberOfDoubles) {
-                        missingValue(key);
-                        continue;
-                    }
-                    switch (count) {
-                        case 0:  continue;
-                        case 1:  value = numericParameters.get(valueOffset); break;
-                        default: final double[] array = new double[count];
-                                 for (int j=0; j<count; j++) {
-                                     array[j] = numericParameters.doubleValue(valueOffset + j);
-                                 }
-                                 value = array;
-                                 break;
-                    }
-                    break;
-                }
-                /*
-                 * ASCII encoding use the pipe ('|') character as a replacement for the NUL character
-                 * used in C/C++ programming languages. We need to omit those trailing characters.
-                 */
-                case Tags.GeoAsciiParams & 0xFFFF: {
-                    int upper = valueOffset + count;
-                    if (upper > numberOfChars) {
-                        missingValue(key);
-                        continue;
-                    }
-                    upper = CharSequences.skipTrailingWhitespaces(asciiParameters, valueOffset, upper);
-                    while (upper > valueOffset && asciiParameters.charAt(upper - 1) == SEPARATOR) {
-                        upper--;    // Skip trailing pipe, interpreted as C/C++ NUL character.
-                    }
-                    // Use String.trim() for skipping C/C++ NUL character in addition of whitespaces.
-                    final String s = asciiParameters.substring(valueOffset, upper).trim();
-                    if (s.isEmpty()) continue;
-                    value = s;
-                    break;
-                }
-                /*
-                 * GeoKeys are not expected to use other storage mechanism. If this happen anyway, report a warning
-                 * and continue on the assumption that if the value that we are ignoring was critical information,
-                 * it would have be stored in one of the standard GeoTIFF tags.
-                 */
-                default: {
-                    warning(Resources.Keys.UnsupportedGeoKeyStorage_1, GeoKeys.name(key));
-                    continue;
-                }
-            }
-            geoKeys.put(key, value);
+        } finally {
+            source.logger = null;
+            this.majorRevision = source.majorRevision;
+            this.minorRevision = source.minorRevision;
         }
         /*
-         * At this point we finished copying all GeoTIFF keys in the 'geoKeys' map. Before to create the CRS,
+         * At this point we finished copying all GeoTIFF keys in the `geoKeys` map. Before to create the CRS,
          * store a few metadata. The first one is an ASCII reference to published documentation on the overall
          * configuration of the GeoTIFF file. In practice it seems to be often the projected CRS name, despite
          * GeoKeys.PCSCitation being already for that purpose.
@@ -1066,7 +896,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      */
     static String[] splitName(final String name) {
         final String[] names = new String[GCRS + 1];
-        final String[] components = (String[]) CharSequences.split(name, SEPARATOR);
+        final String[] components = (String[]) CharSequences.split(name, GeoKeysLoader.SEPARATOR);
         switch (components.length) {
             case 0: break;
             case 1: names[GCRS] = name; break;
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoKeysLoader.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoKeysLoader.java
new file mode 100644
index 0000000..8d929ee
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoKeysLoader.java
@@ -0,0 +1,308 @@
+/*
+ * 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.geotiff;
+
+import java.util.Map;
+import org.apache.sis.math.Vector;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.internal.geotiff.Resources;
+
+
+/**
+ * Loads GeoTIFF keys in a hash map, but without performing any interpretation.
+ * A {@code GeoKeysLoader} receives as inputs the values of the following TIFF tags:
+ *
+ * <ul>
+ *   <li>{@link Tags#GeoKeyDirectory} — array of unsigned {@code short} values grouped into blocks of 4.</li>
+ *   <li>{@link Tags#GeoDoubleParams} — array of {@double} values referenced by {@code GeoKeyDirectory} elements.</li>
+ *   <li>{@link Tags#GeoAsciiParams}  — array of characters referenced by {@code GeoKeyDirectory} elements.</li>
+ * </ul>
+ *
+ * For example, consider the following values for the above-cited tags:
+ *
+ * <table class="sis">
+ *   <caption>GeoKeyDirectory(34735) values</caption>
+ *   <tr><td>    1 </td><td>     1 </td><td>  2 </td><td>     6 </td></tr>
+ *   <tr><td> 1024 </td><td>     0 </td><td>  1 </td><td>     2 </td></tr>
+ *   <tr><td> 1026 </td><td> 34737 </td><td>  0 </td><td>    12 </td></tr>
+ *   <tr><td> 2048 </td><td>     0 </td><td>  1 </td><td> 32767 </td></tr>
+ *   <tr><td> 2049 </td><td> 34737 </td><td> 14 </td><td>    12 </td></tr>
+ *   <tr><td> 2050 </td><td>     0 </td><td>  1 </td><td>     6 </td></tr>
+ *   <tr><td> 2051 </td><td> 34736 </td><td>  1 </td><td>     0 </td></tr>
+ * </table>
+ *
+ * {@preformattext
+ *   GeoDoubleParams(34736) = {1.5}
+ *   GeoAsciiParams(34737) = "Custom File|My Geographic|"
+ * }
+ *
+ * <p>The first number in the {@code GeoKeyDirectory} table indicates that this is a version 1 GeoTIFF GeoKey directory.
+ * This version will only change if the key structure is changed. The other numbers on the first line said that the file
+ * uses revision 1.2 of the set of keys and that there is 6 key values.</p>
+ *
+ * <p>The next line indicates that the first key (1024 = {@code ModelType}) has the value 2 (Geographic),
+ * explicitly placed in the entry list since the TIFF tag location is 0.
+ * The next line indicates that the key 1026 ({@code Citation}) is listed in the {@code GeoAsciiParams(34737)} array,
+ * starting at offset 0 (the first in array), and running for 12 bytes and so has the value "Custom File".
+ * The "|" character is converted to a null delimiter at the end in C/C++ libraries.</p>
+ *
+ * <p>Going further down the list, the key 2051 ({@code GeogLinearUnitSize}) is located in {@code GeoDoubleParams(34736)}
+ * at offset 0 and has the value 1.5; the value of key 2049 ({@code GeogCitation}) is "My Geographic".</p>
+ *
+ * @author  Rémi Maréchal (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+class GeoKeysLoader {
+    /**
+     * Number of {@code short} values in each GeoKey entry.
+     */
+    private static final int ENTRY_LENGTH = 4;
+
+    /**
+     * The character used as a separator in {@link String} multi-values.
+     */
+    static final char SEPARATOR = '|';
+
+    /**
+     * References the {@link GeoKeys} needed for building the Coordinate Reference System.
+     * Can not be null when invoking {@link #load(Map)}.
+     *
+     * @see Tags#GeoKeyDirectory
+     */
+    public Vector keyDirectory;
+
+    /**
+     * The numeric values referenced by the {@link #keyDirectory}.
+     * Can be {@code null} if none.
+     *
+     * @see Tags#GeoDoubleParams
+     */
+    public Vector numericParameters;
+
+    /**
+     * The characters referenced by the {@link #keyDirectory}.
+     * Can be {@code null} if none.
+     *
+     * @see Tags#GeoAsciiParams
+     * @see #setAsciiParameters(String[])
+     */
+    public String asciiParameters;
+
+    /**
+     * Version of the set of keys declared in the {@code GeoKeyDirectory} header.
+     */
+    short majorRevision, minorRevision;
+
+    /**
+     * Where to send warnings, or {@code null} for ignoring warnings silently.
+     * While {@code CRSBuilder} is a class doing complex work (CRS construction),
+     * only the logging-related methods will be invoked by {@code GeoKeysLoader}.
+     */
+    CRSBuilder logger;
+
+    /**
+     * Creates a new GeoTIFF keys loader. The {@link #keyDirectory}, {@link #numericParameters}
+     * {@link #asciiParameters} and {@link #logger} fields must be initialized by the caller.
+     */
+    GeoKeysLoader() {
+    }
+
+    /**
+     * Sets the value of {@link #asciiParameters} from {@link Tags#GeoAsciiParams} value.
+     */
+    final void setAsciiParameters(final String[] values) {
+        switch (values.length) {
+            case 0:  break;
+            case 1:  asciiParameters = values[0]; break;
+            default: asciiParameters = String.join("\u0000", values).concat("\u0000"); break;
+        }
+    }
+
+    /**
+     * Loads GeoKeys and write values in the given map.
+     *
+     * @param  geoKeys  where to write GeoKeys.
+     * @return whether the operation succeed.
+     */
+    final boolean load(final Map<Short,Object> geoKeys) {
+        final int numberOfKeys;
+        final int directoryLength = keyDirectory.size();
+        if (directoryLength >= ENTRY_LENGTH) {
+            final int version = keyDirectory.intValue(0);
+            if (version != 1) {
+                warning(Resources.Keys.UnsupportedGeoKeyDirectory_1, version);
+                return false;
+            }
+            majorRevision = keyDirectory.shortValue(1);
+            minorRevision = keyDirectory.shortValue(2);
+            numberOfKeys  = keyDirectory.intValue(3);
+        } else {
+            numberOfKeys = 0;
+        }
+        /*
+         * The key directory may be longer than needed for the amount of keys, but not shorter.
+         * If shorter, report a warning and stop the parsing since we have no way to know if the
+         * missing information were essentiel or not.
+         *
+         *     (number of key + head) * 4    ---    1 entry = 4 short values.
+         */
+        final int expectedLength = (numberOfKeys + 1) * ENTRY_LENGTH;
+        if (directoryLength < expectedLength) {
+            warning(Resources.Keys.ListTooShort_3, "GeoKeyDirectory", expectedLength, directoryLength);
+            return false;
+        }
+        final int numberOfDoubles = (numericParameters != null) ? numericParameters.size() : 0;
+        final int numberOfChars   =   (asciiParameters != null) ? asciiParameters.length() : 0;
+        /*
+         * Now iterate over all GeoKey values. The values are copied in a HashMap for convenience,
+         * because the CRS creation may use them out of order.
+         */
+        for (int i=1; i <= numberOfKeys; i++) {
+            final int p = i * ENTRY_LENGTH;
+            final short key       = keyDirectory.shortValue(p);
+            final int tagLocation = keyDirectory.intValue(p+1);
+            final int count       = keyDirectory.intValue(p+2);
+            final int valueOffset = keyDirectory.intValue(p+3);
+            if (valueOffset < 0 || count < 0) {
+                missingValue(key);
+                continue;
+            }
+            final Object value;
+            switch (tagLocation) {
+                /*
+                 * tagLocation == 0 means that `valueOffset` actually contains the value,
+                 * thus avoiding the need to allocate a separated storage location for it.
+                 * The count should be 1.
+                 */
+                case 0: {
+                    switch (count) {
+                        case 0:  continue;
+                        case 1:  break;          // Expected value.
+                        default: {
+                            warning(Resources.Keys.UnexpectedListOfValues_2, GeoKeys.name(key), count);
+                            break;
+                        }
+                    }
+                    value = valueOffset;
+                    break;
+                }
+                /*
+                 * Values of type `short` are stored in the same vector than the key directory;
+                 * the specification does not allocate a separated vector for them. We use the
+                 * `int` type if needed for allowing storage of unsigned short values.
+                 */
+                case Tags.GeoKeyDirectory & 0xFFFF: {
+                    if (valueOffset + count > keyDirectory.size()) {
+                        missingValue(key);
+                        continue;
+                    }
+                    switch (count) {
+                        case 0:  continue;
+                        case 1:  value = keyDirectory.get(valueOffset); break;
+                        default: {
+                            final int[] array = new int[count];
+                            for (int j=0; j<count; j++) {
+                                array[j] = keyDirectory.intValue(valueOffset + j);
+                            }
+                            value = array;
+                            break;
+                        }
+                    }
+                    break;
+                }
+                /*
+                 * Values of type `double` are read from a separated vector, `numericParameters`.
+                 * Result is stored in a Double wrapper or in an array of type 'double[]'.
+                 */
+                case Tags.GeoDoubleParams & 0xFFFF: {
+                    if (valueOffset + count > numberOfDoubles) {
+                        missingValue(key);
+                        continue;
+                    }
+                    switch (count) {
+                        case 0:  continue;
+                        case 1:  value = numericParameters.get(valueOffset); break;
+                        default: {
+                            final double[] array = new double[count];
+                            for (int j=0; j<count; j++) {
+                                array[j] = numericParameters.doubleValue(valueOffset + j);
+                            }
+                            value = array;
+                            break;
+                        }
+                    }
+                    break;
+                }
+                /*
+                 * ASCII encoding use the pipe ('|') character as a replacement for the NUL character
+                 * used in C/C++ programming languages. We need to omit those trailing characters.
+                 */
+                case Tags.GeoAsciiParams & 0xFFFF: {
+                    int upper = valueOffset + count;
+                    if (upper > numberOfChars) {
+                        missingValue(key);
+                        continue;
+                    }
+                    upper = CharSequences.skipTrailingWhitespaces(asciiParameters, valueOffset, upper);
+                    while (upper > valueOffset && asciiParameters.charAt(upper - 1) == SEPARATOR) {
+                        upper--;    // Skip trailing pipe, interpreted as C/C++ NUL character.
+                    }
+                    // Use String.trim() for skipping C/C++ NUL character in addition of whitespaces.
+                    final String s = asciiParameters.substring(valueOffset, upper).trim();
+                    if (s.isEmpty()) continue;
+                    value = s;
+                    break;
+                }
+                /*
+                 * GeoKeys are not expected to use other storage mechanism. If this happen anyway, report a warning
+                 * and continue on the assumption that if the value that we are ignoring was critical information,
+                 * it would have be stored in one of the standard GeoTIFF tags.
+                 */
+                default: {
+                    warning(Resources.Keys.UnsupportedGeoKeyStorage_1, GeoKeys.name(key));
+                    continue;
+                }
+            }
+            geoKeys.put(key, value);
+        }
+        return true;
+    }
+
+    /**
+     * Reports a warning with a message built from the given resource keys and arguments.
+     *
+     * @param  key   one of the {@link Resources.Keys} constants.
+     * @param  args  arguments for the log message.
+     */
+    private void warning(final short key, final Object... args) {
+        if (logger != null) {
+            logger.warning(key, args);
+        }
+    }
+
+    /**
+     * Reports a warning about missing value for the given key.
+     */
+    private void missingValue(final short key) {
+        if (logger != null) {
+            logger.missingValue(key);
+        }
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
index baa5a9e..5784516 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
@@ -63,6 +63,7 @@ abstract class GeoTIFF implements Closeable {
 
     /**
      * The store which created this reader or writer.
+     * This is also the synchronization lock.
      */
     final GeoTiffStore store;
 
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 1bc2fe3..29a7046 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -56,6 +56,7 @@ import org.apache.sis.internal.util.ListOfUnknownSize;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -65,7 +66,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Thi Phuong Hao Nguyen (VNSC)
- * @version 1.1
+ * @version 1.2
  * @since   0.8
  * @module
  */
@@ -78,6 +79,8 @@ public class GeoTiffStore extends DataStore implements Aggregate {
 
     /**
      * The GeoTIFF reader implementation, or {@code null} if the store has been closed.
+     *
+     * @see #reader()
      */
     private Reader reader;
 
@@ -126,6 +129,13 @@ public class GeoTiffStore extends DataStore implements Aggregate {
     private Metadata metadata;
 
     /**
+     * The native metadata, or {@code null} if not yet created.
+     *
+     * @see #getNativeMetadata()
+     */
+    private TreeTable nativeMetadata;
+
+    /**
      * Description of images in this GeoTIFF files. This collection is created only when first needed.
      *
      * @see #components()
@@ -327,6 +337,38 @@ public class GeoTiffStore extends DataStore implements Aggregate {
     }
 
     /**
+     * Returns TIFF tags and GeoTIFF keys as a tree for debugging purpose.
+     * The tags and keys appear in the order they are declared in the file.
+     * The columns are tag numerical code as an {@link Integer},
+     * tag name as a {@link String} and value as an {@link Object}.
+     *
+     * <p>This method should not be invoked during normal operations;
+     * the {@linkplain #getMetadata() standard metadata} are preferred
+     * because they allow abstraction of data format details.
+     * Native metadata should be used only when an information does not appear in standard metadata,
+     * or for debugging purposes.</p>
+     *
+     * <h4>Performance note</h4>
+     * Since this method should not be invoked in normal operations, it has not been tuned for performance.
+     * Invoking this method may cause a lot of {@linkplain java.nio.channels.SeekableByteChannel#position(long)
+     * seek operations}.
+     *
+     * @return resources information structured in an implementation-specific way.
+     * @throws DataStoreException if an error occurred while reading the metadata.
+     *
+     * @since 1.2
+     */
+    @Override
+    public synchronized Optional<TreeTable> getNativeMetadata() throws DataStoreException {
+        if (nativeMetadata == null) try {
+            nativeMetadata = new NativeMetadata().read(reader());
+        } catch (IOException e) {
+            throw errorIO(e);
+        }
+        return Optional.of(nativeMetadata);
+    }
+
+    /**
      * Returns the exception to throw when an I/O error occurred.
      */
     private DataStoreException errorIO(final IOException e) {
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
index c8cb55c..ac30ee7 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
@@ -79,7 +79,7 @@ import org.apache.sis.math.Vector;
  * @since   1.0
  * @module
  */
-final class GridGeometryBuilder {
+final class GridGeometryBuilder extends GeoKeysLoader {
     /**
      * The reader for which we will create coordinate reference systems.
      * This is used for reporting warnings.
@@ -92,20 +92,12 @@ final class GridGeometryBuilder {
     ////                                                                                ////
     ////////////////////////////////////////////////////////////////////////////////////////
 
-    /**
-     * References the {@link GeoKeys} needed for building the Coordinate Reference System.
-     */
-    public Vector keyDirectory;
-
-    /**
-     * The numeric values referenced by the {@link #keyDirectory}.
-     */
-    public Vector numericParameters;
-
-    /**
-     * The characters referenced by the {@link #keyDirectory}.
+    /*
+     * Fields inherited from `GeoKeysLoader`:
+     *   - keyDirectory:       references the GeoKeys needed for building the Coordinate Reference System.
+     *   - numericParameters:  the numeric values referenced by the `keyDirectory`.
+     *   - asciiParameters:    the characters referenced by the `keyDirectory`.
      */
-    public String asciiParameters;
 
     /**
      * Raster model tie points. This vector contains coordinate values structured as (I,J,K, X,Y,Z) records.
@@ -288,7 +280,7 @@ final class GridGeometryBuilder {
         if (keyDirectory != null) {
             final CRSBuilder helper = new CRSBuilder(reader);
             try {
-                crs = helper.build(keyDirectory, numericParameters, asciiParameters);
+                crs = helper.build(this);
                 description  = helper.description;
                 cellGeometry = helper.cellGeometry;
             } catch (NoSuchIdentifierException | ParameterNotFoundException e) {
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 2e28037..7600546 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -607,7 +607,7 @@ final class ImageFileDirectory extends DataCube {
              * Compression scheme used on the image data.
              */
             case Tags.Compression: {
-                final long value = type.readLong(input(), count);
+                final int value = type.readInt(input(), count);
                 compression = Compression.valueOf(value);
                 if (compression == Compression.UNKNOWN) {
                     return value;                           // Cause a warning to be reported by the caller.
@@ -807,12 +807,7 @@ final class ImageFileDirectory extends DataCube {
              * Note that TIFF files use 0 as the end delimiter in strings (C/C++ convention).
              */
             case Tags.GeoAsciiParams: {
-                final String[] values = type.readString(input(), count, encoding());
-                switch (values.length) {
-                    case 0:  break;
-                    case 1:  referencing().asciiParameters = values[0]; break;
-                    default: referencing().asciiParameters = String.join("\u0000", values).concat("\u0000"); break;
-                }
+                referencing().setAsciiParameters(type.readString(input(), count, encoding()));
                 break;
             }
             /*
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
new file mode 100644
index 0000000..663cd44
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/NativeMetadata.java
@@ -0,0 +1,249 @@
+/*
+ * 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.geotiff;
+
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.function.IntFunction;
+import java.io.IOException;
+import org.apache.sis.math.Vector;
+import org.apache.sis.util.Numbers;
+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.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.geotiff.Compression;
+import org.apache.sis.internal.geotiff.Predictor;
+
+import static java.lang.Math.addExact;
+import org.apache.sis.util.resources.Vocabulary;
+
+
+/**
+ * View over GeoTIFF tags and GeoTIFF keys in their "raw" form (without interpretation).
+ * Used only when showing {@linkplain GeoTiffStore#getNativeMetadata() native metadata}.
+ *
+ * <p>This implementation is inefficient because it performs a lot of "seek" operations.
+ * This class does not make any effort for reading data in a more sequential way.
+ * The performance penalty should not matter because this class should not be used except
+ * for debugging purposes (the normal use is to interpret tags as they are read).</p>
+ *
+ * <p>This class is thread-safe if the user does not try to write in the tree.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class NativeMetadata extends GeoKeysLoader {
+    /**
+     * Column for the tag number or GeoKey number.
+     */
+    private static final TableColumn<Integer> CODE  = new TableColumn<>(Integer.class,
+            Vocabulary.formatInternational(Vocabulary.Keys.Code));
+
+    /**
+     * Column for the name associated to the tag.
+     * Value may be null if the name is unknown.
+     */
+    private static final TableColumn<CharSequence> NAME = TableColumn.NAME;
+
+    /**
+     * Column for the value associated to the tag.
+     */
+    private static final TableColumn<Object> VALUE = TableColumn.VALUE;
+
+    /**
+     * The stream from which to read the data.
+     */
+    private ChannelDataInput input;
+
+    /**
+     * {@code true} if reading classic TIFF file, or {@code false} for BigTIFF.
+     */
+    private boolean isClassic;
+
+    /**
+     * The node for GeoKeys, or {@code null} if none.
+     *
+     * @see Tags#GeoKeyDirectory
+     */
+    private TreeTable.Node geoNode;
+
+    /**
+     * Creates a reader for a tree table of native metadata.
+     */
+    NativeMetadata() {
+    }
+
+    /**
+     * Reads the tree table content. This method assumes that the caller already verified that the
+     * file is a GeoTIFF file. Tags are keys are added in the order they are declared in the file.
+     */
+    final DefaultTreeTable read(final Reader reader) throws IOException {
+        input     = reader.input;
+        isClassic = reader.intSizeExpansion == 0;
+        final int offsetSize = Integer.BYTES << reader.intSizeExpansion;
+        final DefaultTreeTable table = new DefaultTreeTable(CODE, NAME, VALUE);
+        final TreeTable.Node root = table.getRoot();
+        root.setValue(NAME, "TIFF");
+        input.mark();
+        try {
+            input.seek(addExact(reader.origin, isClassic ? 2*Short.BYTES : 4*Short.BYTES));
+            final Set<Long> doneIFD = new HashSet<>();
+            long nextIFD;
+            /*
+             * Following loop is a simplified copy of `Reader.getImageFileDirectory(int)` method,
+             * without the "deferred entries" mechanism. Instead we seek immediately.
+             */
+            while ((nextIFD = readInt(false)) != 0) {
+                if (!doneIFD.add(nextIFD)) {
+                    // Safety against infinite recursivity.
+                    break;
+                }
+                input.seek(Math.addExact(reader.origin, nextIFD));
+                for (long remaining = readInt(true); --remaining >= 0;) {
+                    final short tag  = (short) input.readUnsignedShort();
+                    final Type type  = Type.valueOf(input.readShort());        // May be null.
+                    final long count = readInt(false);
+                    final long size  = (type != null) ? Math.multiplyExact(type.size, count) : 0;
+                    final long next  = addExact(input.getStreamPosition(), offsetSize);
+                    boolean visible;
+                    /*
+                     * Exclude the tags about location of tiles in the GeoTIFF files.
+                     * Values of those tags are potentially large and rarely useful
+                     * for human reading.
+                     */
+                    switch (tag) {
+                        case Tags.TileOffsets:
+                        case Tags.StripOffsets:
+                        case Tags.TileByteCounts:
+                        case Tags.StripByteCounts: visible = false; break;
+                        default: visible = (size != 0); break;
+                    }
+                    if (visible) {
+                        if (size > offsetSize) {
+                            final long offset = readInt(false);
+                            input.seek(Math.addExact(reader.origin, offset));
+                        }
+                        Object value = null;
+                        switch (tag) {
+                            case Tags.GeoKeyDirectory: {
+                                writeGeoKeys();             // Flush previous keys if any (should never happen).
+                                keyDirectory = type.readVector(input, count);
+                                value = "GeoTIFF";
+                                break;
+                            }
+                            case Tags.GeoDoubleParams: {
+                                numericParameters = type.readVector(input, count);
+                                visible = false;
+                                break;
+                            }
+                            case Tags.GeoAsciiParams: {
+                                setAsciiParameters(type.readString(input, count, reader.store.encoding));
+                                visible = false;
+                                break;
+                            }
+                            default: {
+                                value = type.readObject(input, count);
+                                if (value instanceof Vector) {
+                                    final Vector v = (Vector) value;
+                                    switch (v.size()) {
+                                        case 0: value = null; break;
+                                        case 1: value = v.get(0); break;
+                                    }
+                                }
+                                /*
+                                 * Replace a few numerical values by a more readable string when available.
+                                 * We currently perform this replacement only for tags for which we defined
+                                 * an enumeration.
+                                 */
+                                switch (tag) {
+                                    case Tags.Compression: value = toString(value, Compression::valueOf, Compression.UNKNOWN); break;
+                                    case Tags.Predictor:   value = toString(value,   Predictor::valueOf,   Predictor.UNKNOWN); break;
+                                }
+                            }
+                        }
+                        if (visible) {
+                            final TreeTable.Node node = root.newChild();
+                            node.setValue(CODE,  Short.toUnsignedInt(tag));
+                            node.setValue(NAME,  Tags.name(tag));
+                            node.setValue(VALUE, value);
+                            if (tag == Tags.GeoKeyDirectory) {
+                                geoNode = node;
+                            }
+                        }
+                    }
+                    input.seek(next);
+                }
+            }
+        } catch (ArithmeticException e) {
+            throw new IOException(e);           // Can not seek that far.
+        } finally {
+            input.reset();
+        }
+        writeGeoKeys();
+        return table;
+    }
+
+    /**
+     * Reads the {@code short}, {@code int} or {@code long} value (depending if the
+     * file is standard of big TIFF) at the current {@linkplain Reader#input} position.
+     */
+    private long readInt(final boolean isShort) throws IOException {
+        if (isClassic) {
+            return isShort ? input.readUnsignedShort() : input.readUnsignedInt();
+        } else {
+            final long entry = input.readLong();
+            if (entry < 0) {
+                throw new ArithmeticException();
+            }
+            return entry;
+        }
+    }
+
+    /**
+     * Replaces an integer code by its enumeration value if that value is different than {@code unknown}.
+     */
+    private static Object toString(final Object value, final IntFunction<Enum<?>> valueOf, final Enum<?> unknown) {
+        if (value != null && Numbers.isInteger(value.getClass())) {
+            final Enum<?> c = valueOf.apply(((Number) value).intValue());
+            if (c != unknown) return c.name();
+        }
+        return value;
+    }
+
+    /**
+     * Write child values in {@link #geoNode} if non-null.
+     */
+    private void writeGeoKeys() {
+        if (geoNode != null) {
+            final Map<Short,Object> geoKeys = new LinkedHashMap<>(32);
+            load(geoKeys);
+            for (final Map.Entry<Short,Object> entry : geoKeys.entrySet()) {
+                final TreeTable.Node node = geoNode.newChild();
+                final short code = entry.getKey();
+                node.setValue(CODE,  Short.toUnsignedInt(code));
+                node.setValue(NAME,  GeoKeys.name(code));
+                node.setValue(VALUE, entry.getValue());
+            }
+            geoNode = null;
+        }
+    }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
index 1bf97eb..ef6223a 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
@@ -49,7 +49,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Alexis Manin (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.8
  * @module
  */
@@ -78,7 +78,7 @@ final class Reader extends GeoTIFF {
      * Those values are defined that way for making easier (like a boolean flag) to test if
      * the file is a BigTIFF format, with statement like {@code if (intSizeExpansion != 0)}.
      */
-    private final byte intSizeExpansion;
+    final byte intSizeExpansion;
 
     /**
      * Offset (relative to the beginning of the TIFF file) of the next Image File Directory (IFD)
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Type.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Type.java
index ef59ee0..c7ccbb7 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Type.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Type.java
@@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.math.Fraction;
 import org.apache.sis.math.Vector;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
@@ -36,7 +37,7 @@ import org.apache.sis.util.resources.Errors;
  * This enumeration rather match the Java primitive type names.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.2
  * @since   0.8
  * @module
  */
@@ -52,6 +53,11 @@ enum Type {
         @Override public long readLong(final ChannelDataInput input, final long count) throws IOException {
             throw new UnsupportedOperationException(name());
         }
+
+        /** Unknown value (used for reporting native metadata only). */
+        @Override public Object readObject(final ChannelDataInput input, final long count) throws IOException {
+            return null;
+        }
     },
 
     /**
@@ -290,13 +296,22 @@ enum Type {
      * </ul>
      */
     RATIONAL(10, (2*Integer.BYTES), false) {
-        @Override public double readDouble(final ChannelDataInput input, final long count) throws IOException {
-            final double value = input.readInt() / (double) input.readInt();
+        private Fraction readFraction(final ChannelDataInput input, final long count) throws IOException {
+            final Fraction value = new Fraction(input.readInt(), input.readInt());
             for (long i=1; i<count; i++) {
-                ensureSingleton(value, input.readInt() / (double) input.readInt(), count);
+                ensureSingleton(value.doubleValue(), input.readInt() / (double) input.readInt(), count);
             }
             return value;
         }
+
+        @Override public double readDouble(final ChannelDataInput input, final long count) throws IOException {
+            return readFraction(input, count).doubleValue();
+        }
+
+        /** Returns the value as a {@link Fraction}. */
+        @Override public Object readObject(final ChannelDataInput input, final long count) throws IOException {
+            return readFraction(input, count);
+        }
     },
 
     /**
@@ -307,13 +322,26 @@ enum Type {
      * </ul>
      */
     URATIONAL(5, (2*Integer.BYTES), true) {
-        @Override public double readDouble(final ChannelDataInput input, final long count) throws IOException {
-            final double value = input.readUnsignedInt() / (double) input.readUnsignedInt();
+        private Number readFraction(final ChannelDataInput input, final long count) throws IOException {
+            final long n  = input.readUnsignedInt();
+            final long d  = input.readUnsignedInt();
+            final int  ni = (int) n;
+            final int  di = (int) d;
+            final Number value = (ni == n && di == d) ? new Fraction(ni, di) : Double.valueOf(n / (double) d);
             for (long i=1; i<count; i++) {
-                ensureSingleton(value, input.readUnsignedInt() / (double) input.readUnsignedInt(), count);
+                ensureSingleton(value.doubleValue(), input.readUnsignedInt() / (double) input.readUnsignedInt(), count);
             }
             return value;
         }
+
+        @Override public double readDouble(final ChannelDataInput input, final long count) throws IOException {
+            return readFraction(input, count).doubleValue();
+        }
+
+        /** Returns the value as {@link Faction} if possible or {@link Double} otherwise. */
+        @Override public Object readObject(final ChannelDataInput input, final long count) throws IOException {
+            return readFraction(input, count);
+        }
     },
 
     /**
@@ -363,6 +391,10 @@ enum Type {
         @Override public Object readArray(final ChannelDataInput input, final int count) throws IOException {
             return readString(input, count, StandardCharsets.US_ASCII);
         }
+
+        @Override public Object readObject(final ChannelDataInput input, final long count) throws IOException {
+            return readString(input, count);
+        }
     };
 
     /**
@@ -584,6 +616,20 @@ enum Type {
     }
 
     /**
+     * Returns the value as a {@link Vector}, a {@link Number} (only for fractions) or a {@link String} instance.
+     * This method should be overridden by all enumeration values that do no override
+     * {@link #readArray(ChannelDataInput, int)}.
+     *
+     * @param  input  the input from where to read the values.
+     * @param  count  the amount of values.
+     * @return the value as a Java array or a {@link String}, or {@code null} if undefined.
+     * @throws IOException if an error occurred while reading the stream.
+     */
+    public Object readObject(ChannelDataInput input, long count) throws IOException {
+        return readVector(input, count);
+    }
+
+    /**
      * Reads an arbitrary amount of values as a Java array.
      *
      * @param  input  the input from where to read the values.
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/package-info.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/package-info.java
index caff3c6..208d60a 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/package-info.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/package-info.java
@@ -16,7 +16,7 @@
  */
 
 /**
- * Maps ISO metadata elements from/to the GeoTIFF tags.
+ * Maps GeoTIFF tags to ISO metadata and read raster data as coverages.
  *
  * <p>References:</p>
  * <ul>