sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Reconnect the MetadataOverview panel to the small map showing data position on world.
Date Sat, 02 Nov 2019 16:27:42 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 14bf170  Reconnect the MetadataOverview panel to the small map showing data position
on world.
14bf170 is described below

commit 14bf170da63f23a2e8a36622e2855b2e5d199ad6
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Nov 2 01:08:37 2019 +0100

    Reconnect the MetadataOverview panel to the small map showing data position on world.
---
 .../main/java/org/apache/sis/gui/dataset/Form.java | 251 +++++++++
 .../apache/sis/gui/dataset/MetadataOverview.java   | 626 ++++++++++++---------
 .../apache/sis/gui/dataset/ResourceExplorer.java   |   3 +
 3 files changed, 605 insertions(+), 275 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/Form.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/Form.java
new file mode 100644
index 0000000..8dd90e9
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/Form.java
@@ -0,0 +1,251 @@
+/*
+ * 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.dataset;
+
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.function.IntFunction;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.TilePane;
+import org.opengis.metadata.Metadata;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Base class of pane with data organized as a form.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @param <T> the type of information object (e.g. {@link org.opengis.metadata.identification.Identification}).
+ *
+ * @since 1.1
+ * @module
+ */
+abstract class Form<T> extends GridPane implements EventHandler<ActionEvent>
{
+    /**
+     * Margin to keep around all forms.
+     */
+    private static final Insets PADDING = new Insets(12);
+
+    /**
+     * Number of children per row. This is not necessarily the number of columns in the grid
pane
+     * since we reserve the last column for {@linkplain #pagination}, which span all rows.
+     */
+    static final int NUM_CHILD_PER_ROW = 2;
+
+    /**
+     * For selecting which form to show when there is many. Children are {@link ToggleButton}.
+     * The number of buttons should be the length of {@link #information} array.
+     */
+    private final TilePane pagination;
+
+    /**
+     * The group of {@linkplain #pagination} buttons, for keeping only one of the selected
at a given time.
+     */
+    private final ToggleGroup pageGroup;
+
+    /**
+     * The information to show in the form, or {@code null} if not yet determined.
+     * This array shall not contain any null element.
+     */
+    private T[] information;
+
+    /**
+     * Index of the first child containing the rows added by calls to {@link #addRow(String,
String)} method.
+     * All children from this index until the end of the children list shall be instances
of {@link Label}.
+     */
+    private int rowsStart;
+
+    /**
+     * Index after the last valid child. Should be equal to {@code getChildren().size()}
but may temporarily
+     * differ while we are updating the pane content with new information. The difference
may happen because
+     * we try to recycle existing {@link Label} instances before to discard them and create
new ones.
+     */
+    private int rowsEnd;
+
+    /**
+     * Creates a new form.
+     */
+    Form() {
+        pageGroup  = new ToggleGroup();
+        pagination = new TilePane(Orientation.VERTICAL);
+        pagination.setAlignment(Pos.TOP_RIGHT);
+        pagination.setVgap(9);
+        pagination.setHgap(9);
+        setPadding(PADDING);
+        setVgap(9);
+        setHgap(9);
+        add(pagination, NUM_CHILD_PER_ROW, 0);
+        setVgrow(pagination, Priority.ALWAYS);
+        setHgrow(pagination, Priority.NEVER);
+        getColumnConstraints().setAll(
+            new ColumnConstraints(),     // Let GridPane coputes the width of this column.
+            new ColumnConstraints(100, 300, Double.MAX_VALUE, Priority.ALWAYS, null, true)
+        );
+    }
+
+    /**
+     * Must be invoked by sub-class constructors after the finished construction.
+     */
+    final void finished() {
+        rowsStart = getChildren().size();
+    }
+
+    /**
+     * Sets the information from the given metadata. Subclasses extract the collection of
interest
+     * and delegate to the {@link #setInformation(Collection, IntFunction)} method.
+     */
+    abstract void setInformation(Metadata metadata);
+
+    /**
+     * Specifies a new set of information objects to show. This method takes a snapshot of
+     * collection content, selects an element to show and invoke {@link #buildContent(Object)}.
+     *
+     * @param  info       the information objects to be shown, or an empty collection if
none.
+     * @param  generator  {@code T[]::new}, to be provided by subclasses.
+     */
+    final void setInformation(final Collection<? extends T> info, final IntFunction<T[]>
generator) {
+        information = info.toArray(generator);
+        /*
+         * The array of information should not contain any null element.
+         * However we are better to check. Null elements are removed below.
+         */
+        int n = 0;
+        for (final T e : information) {
+            if (e != null) {
+                information[n++] = e;
+            }
+        }
+        information = ArraysExt.resize(information, n);
+        /*
+         * Adjusts the number of children in the `pagination` control so that the number
of
+         * buttons is the length of `information` array. We try to recycle existing objects.
+         */
+        final ObservableList<Node> pages = pagination.getChildren();
+        int i = pages.size();
+        if (i < n) {
+            final NumberFormat format = getNumberFormat();
+            do {
+                final ToggleButton b = new ToggleButton(format.format(++i));
+                b.setToggleGroup(pageGroup);
+                b.setOnAction​(this);
+                pages.add(b);
+            } while (i < n);
+        } else if (i > n) {
+            pages.subList(n, i).clear();
+        }
+        /*
+         * Update the pane content with the first information.
+         */
+        setVisible(n != 0);
+        if (n != 0) {
+            pageGroup.selectToggle((ToggleButton) pagination.getChildren().get(0));
+            update(0);
+        }
+    }
+
+    /**
+     * Invoked when the user selects a page. This method locates which button has been pressed,
+     * then invokes {@link #update(int)} for the corresponding page of information.
+     */
+    @Override
+    public final void handle(final ActionEvent event) {
+        final ToggleButton source = (ToggleButton) event.getSource();
+        if (source.isSelected()) {
+            final ObservableList<Node> children = pagination.getChildren();
+            final int n = children.size();
+            for (int i=0; i<n; i++) {
+                if (children.get(i) == source) {
+                    update(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Invoked when the pane needs to update its content. This method reset this pane except
for the
+     * pagination control, invoke {@link #buildContent(Object)} then discard any extraneous
labels.
+     * The pagination control will span as many rows as the grid pane has.
+     */
+    private void update(final int index) {
+        rowsEnd = rowsStart;
+        buildContent(information[index]);
+        final ObservableList<Node> children = getChildren();
+        children.subList(rowsEnd, children.size()).clear();
+        setRowSpan(pagination, getRowCount());
+    }
+
+    /**
+     * Invoked when a new information object should be shown in this pane.
+     * Implementer should invoke {@link #addRow(String, String)} method only.
+     *
+     * @param  info   the information object to show (never {@code null}).
+     */
+    abstract void buildContent(T info);
+
+    /**
+     * Adds a row to this form. This method does nothing if the given {@code value} is null.
+     *
+     * @param  label  the label of the row to add.
+     * @param  value  the value associated to the label, or {@code null} if none.
+     */
+    final void addRow(final String label, final String value) {
+        if (value == null) {
+            return;
+        }
+        final Label labelCtrl, valueCtrl;
+        final ObservableList<Node> children = getChildren();
+        if (rowsEnd < children.size()) {
+            labelCtrl = (Label) children.get(rowsEnd);
+            valueCtrl = (Label) children.get(rowsEnd + 1);
+            labelCtrl.setText(label);
+        } else {
+            final int row = (rowsEnd - rowsStart) / NUM_CHILD_PER_ROW;
+            labelCtrl = new Label(label);
+            valueCtrl = new Label();
+            labelCtrl.setLabelFor(valueCtrl);
+            valueCtrl.setWrapText(true);
+            add(labelCtrl, 0, row);
+            add(valueCtrl, 1, row);
+            setValignment(labelCtrl, VPos.TOP);
+            setValignment(valueCtrl, VPos.TOP);
+        }
+        valueCtrl.setText(value);
+        rowsEnd += NUM_CHILD_PER_ROW;
+    }
+
+    /**
+     * Returns the locale-dependent object to use for writing numbers.
+     * Subclasses should return some cached instance.
+     */
+    abstract NumberFormat getNumberFormat();
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/MetadataOverview.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/MetadataOverview.java
index 15c0a9d..6e19310 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/MetadataOverview.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/MetadataOverview.java
@@ -19,30 +19,29 @@ package org.apache.sis.gui.dataset;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.DateFormat;
+import java.text.FieldPosition;
 import java.text.NumberFormat;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
 import java.util.Locale;
+import java.util.Date;
+import java.util.StringJoiner;
 import javafx.application.Platform;
 import javafx.collections.ObservableList;
 import javafx.concurrent.Task;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.geometry.Insets;
+import javafx.geometry.HPos;
 import javafx.scene.Node;
 import javafx.scene.canvas.Canvas;
-import javafx.scene.control.ComboBox;
-import javafx.scene.control.Label;
+import javafx.scene.canvas.GraphicsContext;
 import javafx.scene.control.TitledPane;
 import javafx.scene.image.Image;
-import javafx.scene.layout.GridPane;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
 import javafx.scene.paint.Color;
 import org.opengis.metadata.Metadata;
+import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.CitationDate;
+import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.citation.Party;
 import org.opengis.metadata.citation.Individual;
 import org.opengis.metadata.citation.Organisation;
@@ -51,33 +50,48 @@ import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.metadata.extent.GeographicDescription;
 import org.opengis.metadata.extent.GeographicExtent;
-import org.opengis.metadata.identification.DataIdentification;
 import org.opengis.metadata.identification.Identification;
-import org.opengis.metadata.identification.TopicCategory;
+import org.opengis.metadata.identification.DataIdentification;
+import org.opengis.metadata.identification.ServiceIdentification;
 import org.opengis.metadata.spatial.Dimension;
+import org.opengis.metadata.spatial.CellGeometry;
 import org.opengis.metadata.spatial.SpatialRepresentation;
-import org.opengis.metadata.spatial.SpatialRepresentationType;
 import org.opengis.metadata.spatial.GridSpatialRepresentation;
 import org.opengis.referencing.ReferenceSystem;
+import org.opengis.util.ControlledVocabulary;
 import org.opengis.util.InternationalString;
 import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.gui.BackgroundThreads;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.measure.Longitude;
 import org.apache.sis.util.iso.Types;
+import org.apache.sis.util.logging.Logging;
+
+import static org.apache.sis.internal.util.CollectionsExt.nonNull;
 
 
 /**
  * A panel showing a summary of metadata.
  *
  * @author  Smaniotto Enzo
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
 final class MetadataOverview {
     /**
+     * Minimal size of rectangles to be drawn by {@link IdentificationInfo#drawOnMap(GeographicBoundingBox)}.
+     * If a rectangle is smaller, it will be expanded to this size. We use a minimal size
because otherwise
+     * small rectangles may be practically invisible.
+     */
+    private static final double MIN_RECT_SIZE = 6;
+
+    /**
      * Titles panes for different metadata sections (identification info, spatial information,
<i>etc</i>).
      * This is similar to {@link javafx.scene.control.Accordion} except that we allow an
arbitrary amount
      * of titled panes to be opened in same time.
@@ -95,6 +109,33 @@ final class MetadataOverview {
     private final Locale formatLocale;
 
     /**
+     * The format to use for writing numbers, created when first needed.
+     *
+     * @see #getNumberFormat()
+     */
+    private NumberFormat numberFormat;
+
+    /**
+     * The format to use for writing dates, created when first needed.
+     *
+     * @see #getDateFormat()
+     */
+    private DateFormat dateFormat;
+
+    /**
+     * An image of size 360×180 pixels showing a map of the world.
+     * This is loaded when first needed.
+     *
+     * @see #getWorldMap()
+     */
+    private Image worldMap;
+
+    /**
+     * Whether we already tried to load {@link #worldMap}.
+     */
+    private boolean worldMapLoaded;
+
+    /**
      * The metadata to show, or {@code null} if none.
      * This is set by {@link #setMetadata(Metadata)}.
      */
@@ -102,6 +143,8 @@ final class MetadataOverview {
 
     /**
      * If the metadata can not be obtained, the reason.
+     *
+     * @todo show in this control.
      */
     private Throwable failure;
 
@@ -132,6 +175,26 @@ final class MetadataOverview {
     }
 
     /**
+     * Returns the format to use for writing numbers.
+     */
+    private NumberFormat getNumberFormat() {
+        if (numberFormat == null) {
+            numberFormat = NumberFormat.getInstance(formatLocale);
+        }
+        return numberFormat;
+    }
+
+    /**
+     * Returns the format to use for writing dates.
+     */
+    private DateFormat getDateFormat() {
+        if (dateFormat == null) {
+            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM,
formatLocale);
+        }
+        return dateFormat;
+    }
+
+    /**
      * Fetches the metadata in a background thread and delegates to {@link #setMetadata(Metadata)}
when ready.
      *
      * @param  resource  the resource for which to show metadata, or {@code null}.
@@ -177,8 +240,8 @@ final class MetadataOverview {
         final ObservableList<Node> children = panes.getChildren();
         children.clear();
         if (md != null) {
-            addIfNonEmpty(children, "Identification info",    createIdGridPane());
-            addIfNonEmpty(children, "Spatial representation", createSpatialGridPane());
+            addIfNonEmpty(children, "Identification info",    new IdentificationInfo(), md);
+            addIfNonEmpty(children, "Spatial representation", new RepresentationInfo(), md);
         }
     }
 
@@ -186,8 +249,11 @@ final class MetadataOverview {
      * Adds the given pane to the list of children if the pane is non-null and non-empty.
      * If added, a {@link TitledPane} is created with the given title.
      */
-    private static void addIfNonEmpty(final ObservableList<Node> children, final String
title, final GridPane pane) {
+    private static void addIfNonEmpty(final ObservableList<Node> children, final String
title,
+            final Form<?> pane, final Metadata md)
+    {
         if (pane != null && !pane.getChildren().isEmpty()) {
+            pane.setInformation(md);
             children.add(new TitledPane(title, pane));
         }
     }
@@ -200,322 +266,332 @@ final class MetadataOverview {
      * The same pane can be used for an arbitrary amount of identifications.
      * Each instance is identified by its title.
      */
-    private final class IdentificationInfo extends GridPane implements EventHandler<ActionEvent>
{
+    private final class IdentificationInfo extends Form<Identification> {
+        /**
+         * The canvas where to draw geographic bounding boxes over a world map.
+         * Shall never be null, but need to be recreated for each new map.
+         * A canvas of size (0,0) is available for drawing a new map.
+         *
+         * @see #isWorldMapEmpty()
+         * @see #drawOnMap(GeographicBoundingBox)
+         */
+        private Canvas extentOnMap;
+
         /**
-         * The citation titles, one per {@link Identification} instance to show.
-         * This combo box usually has only one element.
+         * Creates an initially empty view for identification information.
          */
-        private final ComboBox<String> choices;
+        IdentificationInfo() {
+            extentOnMap = new Canvas();                         // Size of (0,0) by default.
+            add(extentOnMap, 0, 0, NUM_CHILD_PER_ROW, 1);
+            setHalignment(extentOnMap, HPos.CENTER);
+            finished();
+        }
 
         /**
-         * Identification of resources. Shall have the same number of elements than {@link
#choices}.
+         * If the world map contains a map from some previous metadata,
+         * discard the old canvas and create a new one.
          */
-        private Identification[] identifications;
+        private void clearWorldMap() {
+            if (!isWorldMapEmpty()) {
+                assert getChildren().get(1) == extentOnMap;
+                getChildren().set(1, extentOnMap = new Canvas());
+                setColumnSpan(extentOnMap, NUM_CHILD_PER_ROW);
+                setRowIndex(extentOnMap, 0);
+                setHalignment(extentOnMap, HPos.CENTER);
+            }
+        }
 
         /**
-         * Creates an initially empty view for identification information.
+         * Returns whether {@link #extentOnMap} is considered empty and available for use.
          */
-        IdentificationInfo() {
-            setPadding(new Insets(10));
-            setVgap(10);
-            setHgap(10);
-            choices = new ComboBox<>();
-            choices.setOnAction(this);
-            add(new Label("Title"), 0, 0); add(choices, 1, 0);
+        private boolean isWorldMapEmpty() {
+            return extentOnMap.getWidth() == 0 && extentOnMap.getHeight() == 0;
         }
 
         /**
-         * Sets the identification information to show.
-         * The given collection usually contains only one element.
-         *
-         * @param  info  identification information, or {@code null} if none.
+         * Returns the format to use for writing numbers.
+         */
+        @Override
+        NumberFormat getNumberFormat() {
+            return MetadataOverview.this.getNumberFormat();
+        }
+
+        /**
+         * Sets the identification information from the given metadata.
          */
-        void setInfo(Collection<? extends Identification> info) {
-            if (info == null) {
-                info = Collections.emptyList();
+        @Override
+        void setInformation(final Metadata metadata) {
+            setInformation(nonNull(metadata.getIdentificationInfo()), Identification[]::new);
+        }
+
+        /**
+         * Invoked when new identification information should be shown.
+         * This method updates all fields in this form with the content
+         * of given identification information.
+         */
+        @Override
+        void buildContent(final Identification info) {
+            clearWorldMap();
+            String text = null;
+            final Citation citation = info.getCitation();
+            if (citation != null) {
+                text = string(citation.getTitle());
+                if (text == null) {
+                    text = Citations.getIdentifier(citation);
+                }
+            }
+            addRow("Title:", text);
+            /*
+             * The abstract, or if there is no abstract the credit as a fallback because
it can provide
+             * some hints about the product. The topic category (climatology, health, etc.)
follows.
+             */
+            String label = "Abstract:";
+            text = string(info.getAbstract());
+            if (text == null) {
+                for (final InternationalString c : nonNull(info.getCredits())) {
+                    text = string(c);
+                    if (text != null) {
+                        label = "Credit:";
+                        break;
+                    }
+                }
+            }
+            addRow(label, text);
+            addRow("Topic Category:", string(nonNull(info.getTopicCategories())));
+            /*
+             * Whether the resource is about data or about a service. If it is about data,
+             * we will not write anything since this will be considered the default.
+             */
+            text = null;
+            if (!(info instanceof DataIdentification)) {
+                if (info instanceof ServiceIdentification) {
+                    text = "Service";
+                } else {
+                    text = "The resource does not specify if it contains data.";
+                }
             }
-            identifications = info.toArray(new Identification[info.size()]);
+            addRow("Type of resource:", text);
+            addRow("Representation:", string(nonNull(info.getSpatialRepresentationTypes())));
             /*
-             * Setup the combo box with the title of all identification.
-             * If no title is found, identifiers are used as fallback.
+             * Select a single, arbitrary date. We take the release or publication date if
available.
+             * If no publication date is found, fallback on the creation date. If no creation
date is
+             * found neither, fallback on the first date regardless its type.
              */
-            int firstWithTitle = -1;
-            final String[] titles = new String[identifications.length];
-            for (int i=0; i<titles.length; i++) {
-                String title = null;
-                final Identification id = identifications[i];
-                if (id != null) {
-                    final Citation citation = id.getCitation();
-                    if (citation != null) {
-                        title = string(citation.getTitle());
-                        if (title == null) {
-                            title = Citations.getIdentifier(citation);
+            if (citation != null) {
+                label = null;
+                Date     date = null;
+                for (final CitationDate c : nonNull(citation.getDates())) {
+                    final Date cd = c.getDate();
+                    if (cd != null) {
+                        final DateType type = c.getDateType();
+                        if (DateType.PUBLICATION.equals(type) || DateType.RELEASED.equals(type))
{
+                            label = "Publication date:";
+                            date  = cd;
+                            break;                      // Take the first publication or
release date.
+                        }
+                        final boolean isCreation = DateType.CREATION.equals(type);
+                        if (date == null || isCreation) {
+                            label = isCreation ? "Creation date:" : "Date:";
+                            date  = cd;     // Fallback date: creation date, or the first
date otherwise.
                         }
                     }
                 }
-                if (title == null) {
-                    title = Vocabulary.getResources(textLocale).getString(Vocabulary.Keys.Untitled);
-                } else if (firstWithTitle < 0) {
-                    firstWithTitle = i;
+                if (date != null) {
+                    addRow(label, getDateFormat().format(date));
                 }
-                titles[i] = title;
             }
             /*
-             * At this point we prepared all titles. If the titles were missing in all objects,
-             * take the first "untitled" element as the initial selection.
+             * Write the first description about the spatio-temporal extent,
+             * then draw all geographic extents on a map.
              */
-            choices.getItems().setAll(titles);
-            if (titles.length != 0) {
-                choices.getSelectionModel().clearAndSelect​(Math.max(firstWithTitle, 0));
+            text = null;
+            Identifier identifier = null;
+            for (final Extent extent : nonNull(info.getExtents())) {
+                if (extent != null) {
+                    if (text == null) {
+                        text = string(extent.getDescription());
+                    }
+                    for (final GeographicExtent ge : nonNull(extent.getGeographicElements()))
{
+                        if (identifier == null && ge instanceof GeographicDescription)
{
+                            identifier = ((GeographicDescription) ge).getGeographicIdentifier();
+                        }
+                        if (ge instanceof GeographicBoundingBox) {
+                            drawOnMap((GeographicBoundingBox) ge);
+                        }
+                    }
+                }
             }
-            handle(null);               // For forcing a refrech of pane content.
+            if (text == null) {
+                text = IdentifiedObjects.toString(identifier);
+            }
+            addRow("Extent:", text);
+            setRowIndex(extentOnMap, getRowCount());
         }
 
         /**
-         * Invoked when the user selected a new title.
+         * Draws the given geographic bounding box on the map. This method can be invoked
many times
+         * if there is many bounding boxes on the same map.
          *
-         * @param  event  ignored, can be null.
+         * @param  bbox  the bounding box to draw.
          */
-        @Override
-        public void handle(final ActionEvent event) {
-            Identification id = null;
-            if (identifications != null) {
-                final int selected = choices.getSelectionModel().getSelectedIndex();
-                if (selected >= 0 && selected < identifications.length) {
-                    id = identifications[selected];
+        private void drawOnMap(final GeographicBoundingBox bbox) {
+            double north = Latitude.clamp(bbox.getNorthBoundLatitude());
+            double south = Latitude.clamp(bbox.getSouthBoundLatitude());
+            double east  =                bbox.getEastBoundLongitude();
+            double west  =                bbox.getWestBoundLongitude();
+            if (!(north >= south) || !Double.isFinite(east) || !Double.isFinite(west))
{
+                return;
+            }
+            if (isWorldMapEmpty()) {
+                final Image image = getWorldMap();
+                if (image == null) {
+                    return;                         // Failed to load the image.
                 }
+                extentOnMap.setWidth (image.getWidth());
+                extentOnMap.setHeight(image.getHeight());
+                extentOnMap.getGraphicsContext2D().drawImage(image, 0, 0);
+            }
+            double x = (Longitude.MAX_VALUE - Longitude.MIN_VALUE)  / 2 + west;
+            double y =  (Latitude.MAX_VALUE -  Latitude.MIN_VALUE)  / 2 - north;
+            double w = east  - west;        // TODO: handle envelope spanning anti-meridian.
+            double h = north - south;
+            if (w < MIN_RECT_SIZE) {
+                x -= (MIN_RECT_SIZE - w) / 2;
+                w  =  MIN_RECT_SIZE;
             }
-            onIdSelected(this, id);
+            if (h < MIN_RECT_SIZE) {
+                y -= (MIN_RECT_SIZE - h) / 2;
+                h  =  MIN_RECT_SIZE;
+            }
+            final GraphicsContext gc = extentOnMap.getGraphicsContext2D();
+            gc.setStroke(Color.DARKBLUE);
+            gc.setGlobalAlpha(0.1);
+            gc.fillRect(x, y, w, h);
+            gc.setGlobalAlpha(1.0);
+            gc.strokeRect(x, y, w, h);
         }
     }
 
     /**
-     * The pane when to show the values of {@link Identification} objects.
-     * The same pane can be used for an arbitrary amount of identifications.
-     * Each instance is identified by its title.
+     * Returns an image of size 360×180 pixels showing a map of the world,
+     * or {@code null} if we failed to load the image.
      */
-    private GridPane createIdGridPane() {
-        final IdentificationInfo info = new IdentificationInfo();
-        info.setInfo(metadata.getIdentificationInfo());
-
-        // Show author information.
-        Collection<? extends Responsibility> contacts = metadata.getContacts();
-        if (false && !contacts.isEmpty()) {     // TODO
-            Responsibility contact = contacts.iterator().next();
-            Collection<? extends Party> parties = contact.getParties();
-            if (!parties.isEmpty()) {
-                Party party = parties.iterator().next();
-                if (party.getName() != null) {
-                    Label partyType = new Label("Party");
-                    Label partyValue = new Label(party.getName().toString());
-                    partyValue.setWrapText(true);
-                    if (party instanceof Organisation) {
-                        partyType.setText("Organisation");
-                    } else if (party instanceof Individual) {
-                        partyType.setText("Author");
-                    }
-                    info.add(partyType,  0, 1);
-                    info.add(partyValue, 1, 2);
-                }
+    private Image getWorldMap() {
+        if (!worldMapLoaded) {
+            worldMapLoaded = true;                  // Set now for avoiding retries in case
of failure.
+            Exception error;
+            try (InputStream in = MetadataOverview.class.getResourceAsStream("WorldMap360x180.png"))
{
+                worldMap = new Image(in);
+                error = worldMap.getException();
+            } catch (IOException e) {
+                error = e;
+            }
+            if (error != null) {
+                Logging.unexpectedException(Logging.getLogger(Modules.APPLICATION), MetadataOverview.class,
"getWorldMap", error);
             }
         }
-        return info;
+        return worldMap;
     }
 
-    private void onIdSelected(final GridPane content, final Identification id) {
-        if (id == null) return;
-        final ObservableList<Node> children = content.getChildren();
-        children.subList(2, children.size()).clear();   // Do not remove the 2 first elements,
which are the combo box.
-
-        int row = 1;
-
-        // Show the abstract or the credit, the topic category, the creation date, the type
of data, the representation system info and also the geographical area.
-        Object ab = id.getAbstract();
-        if (ab != null) {
-            InternationalString abs = (InternationalString) ab;
-            Label crd = new Label("Abstract");
-            Label crdValue = new Label(string(abs));
-            crdValue.setWrapText(true);
-            content.add(crd,      0, row);
-            content.add(crdValue, 1, row++);
-        } else {
-            Collection<? extends InternationalString> credits = id.getCredits();
-            if (!credits.isEmpty()) {
-                InternationalString credit = credits.iterator().next();
-                Label crd = new Label("Credit");
-                Label crdValue = new Label(credit.toString());
-                crdValue.setWrapText(true);
-                content.add(crd,      0, row);
-                content.add(crdValue, 1, row++);
-            }
-        }
 
-        Collection<TopicCategory> tcs = id.getTopicCategories();
-        if (!tcs.isEmpty()) {
-            TopicCategory tc = tcs.iterator().next();
-            Label topicC = new Label("Topic Category");
-            Label topicValue = new Label(tc.toString());
-            topicValue.setWrapText(true);
-            content.add(topicC,     0, row);
-            content.add(topicValue, 1, row++);
-        }
 
-        if (!id.getCitation().getDates().isEmpty()) {
-            CitationDate dateAndType = id.getCitation().getDates().iterator().next();
-            DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM,
formatLocale);
-            String dateStr = dateFormat.format(dateAndType.getDate());
-            String s = dateAndType.getDateType().toString();
-            s = s.replace("DateType[", "");
-            s = s.replace("]", "");
-            Label dt = new Label("Date type: " + s.toLowerCase());
-            Label dtValue = new Label(dateStr);
-            dtValue.setWrapText(true);
-            content.add(dt,      0, row);
-            content.add(dtValue, 1, row++);
-        }
 
-        if (id instanceof DataIdentification) {
-            Label topicC = new Label("Object type");
-            Label topicValue = new Label("Data");
-            topicValue.setWrapText(true);
-            content.add(topicC,     0, row);
-            content.add(topicValue, 1, row++);
-        } else {
-            Label topicC = new Label("Object type");
-            Label topicValue = new Label("Service");
-            topicValue.setWrapText(true);
-            content.add(topicC,     0, row);
-            content.add(topicValue, 1, row++);
-        }
+    /**
+     * The pane where to show the values of {@link SpatialRepresentation} objects.
+     * The same pane can be used for an arbitrary amount of spatial representations.
+     */
+    private final class RepresentationInfo extends Form<SpatialRepresentation> {
+        /**
+         * The reference system, or {@code null} if none.
+         */
+        private ReferenceSystem referenceSystem;
 
-        Collection<SpatialRepresentationType> spatialRepresentationTypes = id.getSpatialRepresentationTypes();
-        Iterator<SpatialRepresentationType> its = spatialRepresentationTypes.iterator();
-        String typeList = "Spatial representation type: ";
-        while (its.hasNext()) {
-            SpatialRepresentationType spatialRepresentationType = its.next();
-            typeList += spatialRepresentationType.toString().toLowerCase(textLocale).replace("spatialrepresentationtype[",
"").replace(']', '\0') + ", ";
+        /**
+         * Creates an initially empty view for spatial representation information.
+         */
+        RepresentationInfo() {
+            finished();
         }
-        if (!typeList.equals("Spatial representation type: ")) {
-            Label list = new Label(typeList.substring(0, typeList.length() - 2));
-            list.setWrapText(true);
-            content.add(list, 0, 5, 2, 1);
+
+        /**
+         * Returns the format to use for writing numbers.
+         */
+        @Override
+        NumberFormat getNumberFormat() {
+            return MetadataOverview.this.getNumberFormat();
         }
 
-        Collection<? extends Extent> exs = id.getExtents();
-        if (!exs.isEmpty()) {
-            Extent ex = exs.iterator().next();
-            Collection<? extends GeographicExtent> ges = ex.getGeographicElements();
-            Iterator<? extends GeographicExtent> it = ges.iterator();
-            while (it.hasNext()) {
-                GeographicExtent ge = it.next();
-                Label geoEx = new Label("Zone");
-                Label geoExValue = new Label(ge.toString());
-                geoExValue.setWrapText(true);
-                if (ge instanceof GeographicBoundingBox) {
-                    geoEx.setText("");
-                    GeographicBoundingBox gbd = (GeographicBoundingBox) ge;
-                    geoExValue.setText("");
-                    Canvas c = createMap(gbd.getNorthBoundLatitude(), gbd.getEastBoundLongitude(),
gbd.getSouthBoundLatitude(), gbd.getWestBoundLongitude());
-                    if (c != null) {
-                        content.add(c, 0, 6, 2, 1);
-                    } else {
-                        geoEx.setText("Impossible to load the map.");
-                        content.add(geoEx,      0, row);
-                        content.add(geoExValue, 1, row++);
-                    }
-                } else if (ge instanceof GeographicDescription) {
-                    geoEx.setText("Geographic description");
-                    GeographicDescription gd = (GeographicDescription) ge;
-                    geoExValue.setText(gd.getGeographicIdentifier().getCode());
+        /**
+         * Sets the spatial representation information from the given metadata.
+         */
+        @Override
+        void setInformation(final Metadata metadata) {
+            referenceSystem = null;
+            setInformation(nonNull(metadata.getSpatialRepresentationInfo()), SpatialRepresentation[]::new);
+            for (final ReferenceSystem rs : nonNull(metadata.getReferenceSystemInfo())) {
+                if (rs != null) {
+                    referenceSystem = rs;
+                    break;
                 }
             }
         }
-    }
-
-    private Canvas createMap(double north, double east, double south, double west) {
-        Canvas can = new Canvas();
-        Image image = null;
-        try (InputStream in = MetadataOverview.class.getResourceAsStream("WorldMap360x180.png"))
{
-            image = new Image(in);
-        } catch (IOException e) {
-            // TODO
-        }
-        if (image.errorProperty().getValue()) {
-            return null;
-        }
 
-        double height = image.getHeight();
-        double width = image.getWidth();
-
-        can.setHeight(height);
-        can.setWidth(width);
-        can.getGraphicsContext2D().drawImage(image, 0, 0, width, height);
-        can.getGraphicsContext2D().setStroke(Color.DARKBLUE);
-        can.getGraphicsContext2D().setGlobalAlpha(0.1);
-        double x = west + width / 2, y = height / 2 - north, w = east - west, h = north -
south;
-        can.getGraphicsContext2D().strokeRect(x, y, w, h);
-        final double minRectSize = 6.0;
-        if (w < minRectSize) {
-            double difX = minRectSize - w;
-            x -= difX / 2;
-            w = minRectSize;
-        }
-        if (h < minRectSize) {
-            double difY = minRectSize - h;
-            y -= difY / 2;
-            h = minRectSize;
+        /**
+         * Invoked when new spatial representation information should be shown.
+         * This method updates all fields in this form with the content of given information.
+         */
+        @Override
+        void buildContent(final SpatialRepresentation info) {
+            addRow("Reference system:", IdentifiedObjects.getName(referenceSystem, null));
+            if (info instanceof GridSpatialRepresentation) {
+                final GridSpatialRepresentation sr = (GridSpatialRepresentation) info;
+                final StringBuffer buffer = new StringBuffer();
+                for (final Dimension dim : nonNull(sr.getAxisDimensionProperties())) {
+                    getNumberFormat().format(dim.getDimensionSize(), buffer, new FieldPosition(0));
+                    buffer.append(' ').append(string(Types.getCodeTitle(dim.getDimensionName()))).append("
× ");
+                }
+                if (buffer.length() != 0) {
+                    buffer.setLength(buffer.length() - 3);
+                    addRow("Dimensions:", buffer.toString());
+                }
+                final CellGeometry cg = sr.getCellGeometry();
+                if (cg != null) {
+                    addRow("Cell geometry:", string(Types.getCodeTitle(cg)));
+                }
+            }
         }
-        can.getGraphicsContext2D().fillRect(x, y, w, h);
-        can.getGraphicsContext2D().setGlobalAlpha(1.0);
-        can.getGraphicsContext2D().setStroke(Color.DARKBLUE);
-        can.getGraphicsContext2D().strokeRect(x, y, w, h);
-
-        return can;
     }
 
-    private GridPane createSpatialGridPane() {
-        GridPane gp = new GridPane();
-        gp.setHgap(10.00);
-        gp.setVgap(10.00);
-        int j = 0, k = 1;
-
-        Collection<? extends ReferenceSystem> referenceSystemInfos = metadata.getReferenceSystemInfo();
-        if (!referenceSystemInfos.isEmpty()) {
-            ReferenceSystem referenceSystemInfo = referenceSystemInfos.iterator().next();
-            Label rsiValue = new Label("Reference system infos: " + referenceSystemInfo.getName().toString());
-            rsiValue.setWrapText(true);
-            gp.add(rsiValue, j, k++);
-        }
-
-        Collection<? extends SpatialRepresentation> sris = this.metadata.getSpatialRepresentationInfo();
-        if (sris.isEmpty()) {
-            return gp;
-        }
-        NumberFormat numberFormat = NumberFormat.getIntegerInstance(formatLocale);
-        for (SpatialRepresentation sri : sris) {
-            String currentValue = "• ";
-            if (sri instanceof GridSpatialRepresentation) {
-                GridSpatialRepresentation sr = (GridSpatialRepresentation) sri;
-
-                Iterator<? extends Dimension> it = sr.getAxisDimensionProperties().iterator();
-                while (it.hasNext()) {
-                    Dimension dim = it.next();
-                    currentValue += numberFormat.format(dim.getDimensionSize()) + " " + Types.getCodeTitle(dim.getDimensionName())
+ " * ";
-                }
-                currentValue = currentValue.substring(0, currentValue.length() - 3);
-                Label spRep = new Label(currentValue);
-                gp.add(spRep, j, k++, 2, 1);
-                if (sr.getCellGeometry() != null) {
-                    Label cellGeo = new Label("Cell geometry:");
-                    Label cellGeoValue = new Label(Types.getCodeTitle(sr.getCellGeometry()).toString());
-                    cellGeoValue.setWrapText(true);
-                    gp.add(cellGeo, j, k);
-                    gp.add(cellGeoValue, ++j, k++);
-                    j = 0;
+    /**
+     * @todo
+     */
+    private void createContact() {
+        for (final Responsibility contact : nonNull(metadata.getContacts())) {
+            for (final Party party : nonNull(contact.getParties())) {
+                final String name = string(party.getName());
+                if (name != null) {
+                    String partyType = "Party";
+                    if (party instanceof Organisation) {
+                        partyType = "Organisation";
+                    } else if (party instanceof Individual) {
+                        partyType = "Author";
+                    }
+                    // TODO
                 }
             }
         }
-        return gp;
+    }
+
+    /**
+     * Returns all code lists in a comma-separated list.
+     */
+    private String string(final Collection<? extends ControlledVocabulary> codes) {
+        final StringJoiner buffer = new StringJoiner(", ");
+        for (final ControlledVocabulary c : codes) {
+            final String text = string(Types.getCodeTitle(c));
+            if (text != null) buffer.add(text);
+        }
+        return buffer.length() != 0 ? buffer.toString() : null;
     }
 
     /**
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 57c97a4..49b874d 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
@@ -59,6 +59,9 @@ public class ResourceExplorer {
         pane      = new SplitPane();
         pane.getItems().setAll(resources, metadata.getView());
         resources.getSelectionModel().getSelectedItems().addListener(this::selectResource);
+        SplitPane.setResizableWithParent(resources, Boolean.FALSE);
+        SplitPane.setResizableWithParent(metadata.getView(), Boolean.TRUE);
+        pane.setDividerPosition(0, 300);
     }
 
     /**


Mime
View raw message