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: Start drafting a Canvas class as the common abstraction for implementations that manage the display of MapLayer instances.
Date Wed, 05 Feb 2020 22:37:51 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 c936bda  Start drafting a Canvas class as the common abstraction for implementations
that manage the display of MapLayer instances.
c936bda is described below

commit c936bda5552792194c733889d7014ebfa0519d14
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Feb 5 23:35:43 2020 +0100

    Start drafting a Canvas class as the common abstraction for implementations that manage
the display of MapLayer instances.
---
 .../java/org/apache/sis/internal/map/Canvas.java   | 147 +++++++++++++++++++++
 .../java/org/apache/sis/internal/map/Canvas2D.java |  38 ++++++
 .../org/apache/sis/internal/map/GridCanvas.java    |  37 +++---
 .../java/org/apache/sis/internal/map/MapItem.java  | 124 ++---------------
 .../org/apache/sis/internal/map/MapLayers.java     |  39 +++---
 .../org/apache/sis/internal/map/Observable.java    | 144 ++++++++++++++++++++
 .../org/apache/sis/internal/map/Presentation.java  |  19 +--
 7 files changed, 386 insertions(+), 162 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java
new file mode 100644
index 0000000..247da9b
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas.java
@@ -0,0 +1,147 @@
+/*
+ * 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.internal.map;
+
+import java.util.Objects;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Common abstraction for implementations that manage the display and user manipulation
+ * of {@link MapLayer} instances. This base class makes no assumption about the geometry
+ * of the display device (e.g. flat video monitor using Cartesian coordinate system, or
+ * planetarium dome using spherical coordinate system).
+ *
+ * <p>A newly constructed {@code Canvas} is initial empty. To make something appears,
at least one
+ * {@link MapLayer} must be added. The visual content depends on the {@link MapLayer} data
and associated style.
+ * The contents are usually symbols, features or images, but some implementations can also
manage non-geographic
+ * elements like a map scale.</p>
+ *
+ * <p>There is three {@linkplain CoordinateReferenceSystem coordinate reference systems}
+ * involved (at least conceptually) in rendering of geospatial data:</p>
+ *
+ * <ol class="verbose">
+ *   <li>The <cite>data CRS</cite> is specific to the data to be displayed.
+ *       It may be anything convertible to the <cite>objective CRS</cite>.
+ *       Different {@link MapItem} instances may use different data CRS,
+ *       potentially with a different number of dimensions.</li>
+ *   <li>The {@linkplain #getObjectiveCRS objective CRS} is the common CRS in which
all data
+ *       are converted before to be displayed. If the objective CRS involves a map projection,
+ *       it determines the deformation of shapes that user will see on the display device.
+ *       The objective CRS should have the same number of dimensions than the display device
+ *       (often 2). Its domain of validity should be wide enough for encompassing all data.
+ *       The {@link org.apache.sis.referencing.CRS#suggestCommonTarget CRS.suggestCommonTarget(…)}
+ *       method may be helpful for choosing an objective CRS from a set of data CRS.</li>
+ *   <li>The <cite>device CRS</cite> is the coordinate system of the display
device.
+ *       The conversion from <cite>objective CRS</cite> to <cite>display
CRS</cite> should
+ *       be an affine transform with a scale, a translation and optionally a rotation.</li>
+ * </ol>
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Canvas extends Observable {
+    /**
+     * The {@value} property name, used for notifications about changes in objective CRS.
+     * Associated values are instances of {@link CoordinateReferenceSystem}.
+     *
+     * @see #getObjectiveCRS()
+     * @see #setObjectiveCRS(CoordinateReferenceSystem)
+     * @see #addPropertyChangeListener(String, PropertyChangeListener)
+     */
+    public static final String OBJECTIVE_CRS_PROPERTY = "objectiveCRS";
+
+    /**
+     * The coordinate reference system to display.
+     * If {@code null}, no transformation is applied on data CRS.
+     *
+     * @see #getObjectiveCRS()
+     */
+    private CoordinateReferenceSystem objectiveCRS;
+
+    /**
+     * The conversion (usually affine) from objective CRS to display coordinate system.
+     * The number of source dimensions shall be the number of dimensions of {@link #objectiveCRS}.
+     * The number of target dimensions shall be the number of dimensions of the display device
(usually 2).
+     * This transform shall never be {@code null}.
+     *
+     * @see #getObjectiveToDisplay()
+     */
+    private LinearTransform objectiveToDisplay;
+
+    /**
+     * Creates a new canvas for an output device having the specified number of dimensions.
+     *
+     * @param  dimension  the number of dimensions of the objective CRS.
+     */
+    protected Canvas(final int dimension) {
+        ArgumentChecks.ensureStrictlyPositive("dimension", dimension);
+        objectiveToDisplay = MathTransforms.identity(dimension);
+    }
+
+    /**
+     * Returns the Coordinate Reference System in which all data are transformed before displaying.
+     * The coordinate system of that CRS (Cartesian or spherical) should be related to the
display
+     * device coordinate system with only a final scale, a translation and optionally a rotation
+     * to add.
+     *
+     * @return the Coordinate Reference System in which to transform all data before displaying.
+     *
+     * @see #OBJECTIVE_CRS_PROPERTY
+     */
+    public CoordinateReferenceSystem getObjectiveCRS() {
+        return objectiveCRS;
+    }
+
+    /**
+     * Sets the Coordinate Reference System in which all data are transformed before displaying.
+     * If the given value is different than the previous value, then a change event is sent
to
+     * all listeners registered for the {@value #OBJECTIVE_CRS_PROPERTY} property.
+     *
+     * @param  newValue  the new Coordinate Reference System in which to transform all data
before displaying.
+     * @throws NullPointerException if the given CRS is null.
+     * @throws MismatchedDimensionException if the given CRS does not have the number of
dimensions of the display device.
+     * @throws RenderException if the objective CRS can not be set to the given value for
another reason.
+     */
+    public void setObjectiveCRS(final CoordinateReferenceSystem newValue) throws RenderException
{
+        ArgumentChecks.ensureNonNull(OBJECTIVE_CRS_PROPERTY, newValue);
+        ArgumentChecks.ensureDimensionMatches(OBJECTIVE_CRS_PROPERTY, objectiveToDisplay.getSourceDimensions(),
newValue);
+        final CoordinateReferenceSystem oldValue = objectiveCRS;
+        if (!Objects.equals(oldValue, newValue)) {
+            objectiveCRS = newValue;
+            firePropertyChange(OBJECTIVE_CRS_PROPERTY, oldValue, newValue);
+        }
+    }
+
+    /**
+     * Returns the conversion from objective CRS to display coordinate system, usually as
an affine transform.
+     * The number of source and target dimensions is the number of dimensions of the display
device (usually 2).
+     * This conversion will change every time that the user applies a zoom or a translation
on the viewed data.
+     *
+     * @return conversion (usually affine) from objective CRS to display coordinate system.
+     */
+    public LinearTransform getObjectiveToDisplay() {
+        return objectiveToDisplay;
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas2D.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas2D.java
new file mode 100644
index 0000000..50077f4
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Canvas2D.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal.map;
+
+
+/**
+ * A canvas in which data are reduced to a two-dimensional slice before to be displayed.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class Canvas2D extends Canvas {
+    /**
+     * Creates a new two-dimensional canvas.
+     */
+    protected Canvas2D() {
+        super(2);
+    }
+
+    // TODO: methods to be provided.
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/GridCanvas.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/GridCanvas.java
index 38c40c5..f4baca6 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/GridCanvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/GridCanvas.java
@@ -33,7 +33,6 @@ import org.apache.sis.referencing.operation.DefaultConversion;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Utilities;
-import org.apache.sis.util.collection.BackingStoreException;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.SingleCRS;
@@ -44,18 +43,18 @@ import org.opengis.util.FactoryException;
 
 
 /**
- * Area when an grid coverage is displayed.
+ * Canvas defined by a {@link GridGeometry}.
  *
  * <p>
  * NOTE: this class is a first draft subject to modifications.
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 2.0
- * @since   2.0
+ * @version 1.1
+ * @since   1.1
  * @module
  */
-public abstract class GridCanvas {
+public abstract class GridCanvas extends Canvas2D {
     /**
      * The operation method used by {@link #getDisplayCRS()}.
      * This is a temporary constant, as we will probably need to replace the creation
@@ -68,6 +67,9 @@ public abstract class GridCanvas {
     private GridGeometry gridGeometry2d = gridGeometry;
     private boolean proportion = true;
 
+    protected GridCanvas() {
+    }
+
     public GridGeometry getGridGeometry() {
         return gridGeometry;
     }
@@ -77,17 +79,15 @@ public abstract class GridCanvas {
      *
      * @param gridGeometry new grid geometry
      */
-    public final void setGridGeometry(GridGeometry gridGeometry) throws FactoryException
{
+    public final void setGridGeometry(GridGeometry gridGeometry) throws RenderException {
         ArgumentChecks.ensureNonNull("gridGeometry", gridGeometry);
         if (this.gridGeometry.equals(gridGeometry)) return;
-        final GridGeometry old = this.gridGeometry;
         this.gridGeometry = gridGeometry;
-
         {
             final CoordinateReferenceSystem crs = gridGeometry.getCoordinateReferenceSystem();
             if (crs.getCoordinateSystem().getDimension() == 2) {
                 gridGeometry2d = gridGeometry;
-            } else {
+            } else try {
                 final CoordinateReferenceSystem crs2d = getHorizontalComponent(crs);
                 final int idx = getHorizontalIndex(crs);
                 final MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER);
@@ -98,6 +98,8 @@ public abstract class GridCanvas {
                 //we are expecting axis index to be preserved from grid to crs
                 final GridExtent extent = gridGeometry.getExtent().reduce(idx, idx+1);
                 gridGeometry2d = new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCRS2D,
crs2d);
+            } catch (FactoryException e) {
+                throw new RenderException(e);
             }
         }
     }
@@ -138,7 +140,7 @@ public abstract class GridCanvas {
         return displayCRS;
     }
 
-    public final void setObjectiveCRS(final CoordinateReferenceSystem crs) throws TransformException,
FactoryException {
+    public final void setObjectiveCRS(final CoordinateReferenceSystem crs) throws RenderException
{
         ArgumentChecks.ensureNonNull("Objective CRS", crs);
         if (Utilities.equalsIgnoreMetadata(gridGeometry.getCoordinateReferenceSystem(), crs))
{
             return;
@@ -218,7 +220,7 @@ public abstract class GridCanvas {
         return bounds;
     }
 
-    public void setDisplayBounds(Rectangle2D bounds) {
+    public void setDisplayBounds(Rectangle2D bounds) throws RenderException {
         ArgumentChecks.ensureNonNull("Display bounds", bounds);
 
         final GridGeometry gridGeometry = getGridGeometry();
@@ -234,13 +236,7 @@ public abstract class GridCanvas {
         final GridExtent newExt = new GridExtent(null, low, high, true);
 
         final GridGeometry newGrid = new GridGeometry(newExt, PixelInCell.CELL_CENTER, gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER),
gridGeometry.getCoordinateReferenceSystem());
-        try {
-            setGridGeometry(newGrid);
-        } catch (FactoryException ex) {
-            //we are just changing the size, this should not cause the exception
-            //should not happen with current parameters
-            throw new BackingStoreException(ex.getMessage(), ex);
-        }
+        setGridGeometry(newGrid);
     }
 
     private GridGeometry preserverRatio(GridGeometry gridGeometry) {
@@ -309,7 +305,7 @@ public abstract class GridCanvas {
      * @param targetCRS target CoordinateReferenceSystem
      * @return transformed envelope
      */
-    private static Envelope transform(Envelope env, CoordinateReferenceSystem targetCRS)
throws TransformException{
+    private static Envelope transform(Envelope env, CoordinateReferenceSystem targetCRS)
throws RenderException {
         try {
             return Envelopes.transform(env, targetCRS);
         } catch (TransformException ex) {
@@ -350,8 +346,9 @@ public abstract class GridCanvas {
                     break targetLoop;
                 } catch (FactoryException ex) {
                     //we tried...
+                } catch (TransformException ex) {
+                    throw new RenderException(ex);
                 }
-
                 targetAxeIndex += targetPartDimension;
             }
             sourceAxeIndex += sourcePartDimension;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapItem.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapItem.java
index af19c94..9cc7289 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapItem.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapItem.java
@@ -18,14 +18,9 @@ package org.apache.sis.internal.map;
 
 import java.util.Map;
 import java.util.HashMap;
-import java.util.Arrays;
 import java.util.Objects;
-import java.util.ConcurrentModificationException;
-import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import org.opengis.util.InternationalString;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ArraysExt;
 
 
 /**
@@ -45,15 +40,16 @@ import org.apache.sis.util.ArraysExt;
  * <h2>Synchronization</h2>
  * {@code MapItem} instances are not thread-safe. Synchronization, if desired, is caller
responsibility.
  *
- * @todo Rename as {@code MapNode}? "Item" suggests an element in a list, while {@link MapLayers}
actually
- *       creates a tree.
+ * @todo Rename as {@code LayerNode}? "Item" suggests an element in a list, while {@link
MapLayers} actually
+ *       creates a tree. Furthermore having {@code Layer} in the name would add emphasis
that this is a tree
+ *       of layers and not a tree of arbitrary objects.
  *
  * @author  Johann Sorel (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-public abstract class MapItem {
+public abstract class MapItem extends Observable {
     /**
      * The {@value} property name, used for notifications about changes in map item identifier.
      * The identifier (or name) can be used to reference the item externally.
@@ -138,14 +134,6 @@ public abstract class MapItem {
     private Map<String,Object> userMap;
 
     /**
-     * The registered listeners for each property, created when first needed.
-     *
-     * @see #addPropertyChangeListener(String, PropertyChangeListener)
-     * @see #removePropertyChangeListener(String, PropertyChangeListener)
-     */
-    private Map<String,PropertyChangeListener[]> listeners;
-
-    /**
      * Only used by classes in this package.
      */
     MapItem() {
@@ -154,11 +142,11 @@ public abstract class MapItem {
 
     /**
      * Returns the identifier of this map item. The identifier can be any character string
at developer choice;
-     * there is currently no restriction on the identifier form and no restriction about
identifier uniqueness.
-     * The identifier is currently not used by Apache SIS; it is made available as a user
convenience for
+     * there is currently no restriction on identifier syntax and no restriction about identifier
uniqueness.
+     * That identifier is currently not used by Apache SIS; it is made available as a user
convenience for
      * referencing {@code MapItem} instances externally.
      *
-     * <p>NOTE: restriction about identifier form and uniqueness may be added in a
future version.</p>
+     * <p>NOTE: restriction about identifier syntax and uniqueness may be added in
a future version.</p>
      *
      * @return identifier, or {@code null} if none.
      *
@@ -244,6 +232,8 @@ public abstract class MapItem {
      * then a {@code false} visibility status implies that all group components are also
hidden.
      *
      * @return {@code true} if this item is visible.
+     *
+     * @see #VISIBLE_PROPERTY
      */
     public boolean isVisible() {
         return visible;
@@ -285,100 +275,4 @@ public abstract class MapItem {
         }
         return userMap;
     }
-
-    /**
-     * Register a listener for the property of the given name.
-     * The listener will be notified every time that the property of the given name got a
new value.
-     * The {@code propertyName} can be one of the following values:
-     *
-     * <ul>
-     *   <li>{@value #IDENTIFIER_PROPERTY} — for changes in identifier of this map
item.</li>
-     *   <li>{@value #TITLE_PROPERTY}      — for changes in human-readable short
description.</li>
-     *   <li>{@value #ABSTRACT_PROPERTY}   — for changes in narrative description.</li>
-     *   <li>{@value #VISIBLE_PROPERTY}    — for changes in visibility state.</li>
-     *   <li>Any other property defined by subclasses.</li>
-     * </ul>
-     *
-     * If the same listener is registered twice for the same property, then it will be notified
twice
-     * (this method does not perform duplication checks).
-     *
-     * @param  propertyName  name of the property to listen.
-     * @param  listener      property listener to register.
-     */
-    public final void addPropertyChangeListener(final String propertyName, final PropertyChangeListener
listener) {
-        ArgumentChecks.ensureNonEmpty("propertyName", propertyName);
-        ArgumentChecks.ensureNonNull("listener", listener);
-        if (listeners == null) {
-            listeners = new HashMap<>(4);       // Assume few properties will be listened.
-        }
-        final PropertyChangeListener[] oldList = listeners.get(propertyName);
-        final PropertyChangeListener[] newList;
-        final int n;
-        if (oldList != null) {
-            n = oldList.length;
-            newList = Arrays.copyOf(oldList, n+1);
-        } else {
-            n = 0;
-            newList = new PropertyChangeListener[1];
-        }
-        newList[n] = listener;
-        if (!listeners.replace(propertyName, oldList, newList)) {
-            // Opportunistic safety against some multi-threading misuse.
-            throw new ConcurrentModificationException();
-        }
-    }
-
-    /**
-     * Unregister a property listener. The given {@code propertyName} can be any of the name
documented in
-     * {@link #addPropertyChangeListener(String, PropertyChangeListener)}. If the specified
listener is not
-     * registered for the specified property, then nothing happen. If the listener has been
registered twice,
-     * then only one registration is removed (one registration will remain).
-     *
-     * @param  propertyName  name of the listened property.
-     * @param  listener      property listener to unregister.
-     */
-    public final void removePropertyChangeListener(final String propertyName, final PropertyChangeListener
listener) {
-        ArgumentChecks.ensureNonEmpty("propertyName", propertyName);
-        ArgumentChecks.ensureNonNull("listener", listener);
-        if (listeners != null) {
-            final PropertyChangeListener[] oldList = listeners.get(propertyName);
-            if (oldList != null) {
-                for (int i=oldList.length; --i >= 0;) {
-                    if (oldList[i] == listener) {
-                        if (oldList.length != 1) {
-                            final PropertyChangeListener[] newList = ArraysExt.remove(oldList,
i, 1);
-                            if (listeners.replace(propertyName, oldList, newList)) {
-                                return;
-                            }
-                        } else if (listeners.remove(propertyName, oldList)) {
-                            return;
-                        }
-                        // Opportunistic safety against some multi-threading misuse.
-                        throw new ConcurrentModificationException();
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Notifies all registered listener that a property of the given name changed its value.
-     * It is caller responsibility to verify that the old and new values are not equal
-     * (this method does not verify).
-     *
-     * @param  propertyName  name of the property that changed its value.
-     * @param  oldValue      the old property value (may be {@code null}).
-     * @param  newValue      the new property value (may be {@code null}).
-     */
-    protected void firePropertyChange(final String propertyName, final Object oldValue, final
Object newValue) {
-        if (listeners != null) {
-            final PropertyChangeListener[] list = listeners.get(propertyName);
-            if (list != null) {
-                final PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName,
oldValue, newValue);
-                for (final PropertyChangeListener listener : list) {
-                    listener.propertyChange(event);
-                }
-            }
-        }
-    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapLayers.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapLayers.java
index d7e069d..2ea1bc1 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapLayers.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/MapLayers.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.Objects;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.storage.DataSet;
 
 
@@ -32,10 +33,11 @@ import org.apache.sis.storage.DataSet;
  * Since {@link MapLayer} and {@code MapLayers} are the only {@link MapItem} subclasses,
  * all leaves in this tree can only be {@link MapLayer} instances (assuming no {@code MapLayers}
is empty).
  *
- * <p>A {@code MapLayers} is often (but not necessarily) the root node of the tree
of all graphic elements to
- * draw on the map. The {@link MapItem} children are listed by {@link #getComponents()} in
<var>z</var> order.
- * In addition, {@code MapLayers} defines the {@linkplain #getAreaOfInterest() area of interest}
which should
- * be zoomed by default when the map is rendered.</p>
+ * <p>A {@code MapLayers} is the root node of the tree of all layers to draw on the
map,
+ * unless there is only one layer to draw.
+ * The {@link MapItem} children are listed by {@link #getComponents()} in <var>z</var>
order.
+ * In addition, {@code MapLayers} may define an {@linkplain #getAreaOfInterest() area of
interest}
+ * which should be zoomed by default when the map is rendered.</p>
  *
  * @author  Johann Sorel (Geomatys)
  * @version 1.1
@@ -62,25 +64,25 @@ public class MapLayers extends MapItem {
     /**
      * The area of interest, or {@code null} is unspecified.
      */
-    private Envelope areaOfInterest;
+    private ImmutableEnvelope areaOfInterest;
 
     /**
-     * Creates an initially empty group of graphic elements.
+     * Creates an initially empty group of layers.
      */
     public MapLayers() {
         components = new ArrayList<>();
     }
 
     /**
-     * Gets the modifiable list of components contained in this group.
-     * The components in the list are presented in rendering order.
-     * This means that the first rendered component, which will be below
-     * all other components on the rendered map, is located at index zero.
+     * Gets the modifiable list of children contained in this group.
+     * The elements in the list are sorted in rendering order.
+     * This means that the first rendered element, which will be below
+     * all other elements on the rendered map, is located at index zero.
      *
      * <p>The returned list is modifiable: changes in the returned list will
-     * be immediately reflected in this {@code MapGroup}, and conversely.</p>
+     * be immediately reflected in this {@code MapLayers}, and conversely.</p>
      *
-     * @return modifiable list of components in this group.
+     * @return modifiable list of children in this group of layers.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<MapItem> getComponents() {
@@ -93,9 +95,9 @@ public class MapLayers extends MapItem {
      * since one may want to zoom in a different spatiotemporal area.
      *
      * <p>The {@linkplain org.apache.sis.geometry.GeneralEnvelope#getCoordinateReferenceSystem()
envelope CRS}
-     * defines the map projection to use for rendering the map. It may be different than
the CRS of the data.
-     * The returned envelope may have {@linkplain org.apache.sis.geometry.GeneralEnvelope#isAllNaN()
all its
-     * coordinates set to NaN} if only the {@link CoordinateReferenceSystem} is specified.</p>
+     * provides the reference system to use by default for rendering the map. It may be different
than the CRS
+     * of data. The returned envelope may have {@linkplain org.apache.sis.geometry.GeneralEnvelope#isAllNaN()
+     * all its coordinates set to NaN} if only the {@link CoordinateReferenceSystem} is specified.</p>
      *
      * @return map area to show by default, or {@code null} is unspecified.
      *
@@ -113,10 +115,11 @@ public class MapLayers extends MapItem {
      * @param  newValue  new map area to show by default, or {@code null} is unspecified.
      */
     public void setAreaOfInterest(final Envelope newValue) {
+        final ImmutableEnvelope imenv = ImmutableEnvelope.castOrCopy(newValue);
         final Envelope oldValue = areaOfInterest;
-        if (!Objects.equals(oldValue, newValue)) {
-            areaOfInterest = newValue;
-            firePropertyChange(AREA_OF_INTEREST_PROPERTY, oldValue, newValue);
+        if (!Objects.equals(oldValue, imenv)) {
+            areaOfInterest = imenv;
+            firePropertyChange(AREA_OF_INTEREST_PROPERTY, oldValue, imenv);
         }
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Observable.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Observable.java
new file mode 100644
index 0000000..54436d7
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Observable.java
@@ -0,0 +1,144 @@
+/*
+ * 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.internal.map;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Parent class of all objects for which it is possible to register listeners.
+ * This base class does not need to be public; its methods will appear as if
+ * they were defined directly in sub-classes.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+abstract class Observable {
+    /**
+     * The registered listeners for each property, created when first needed.
+     *
+     * @see #addPropertyChangeListener(String, PropertyChangeListener)
+     * @see #removePropertyChangeListener(String, PropertyChangeListener)
+     */
+    private Map<String,PropertyChangeListener[]> listeners;
+
+    /**
+     * Only used by classes in this package.
+     */
+    Observable() {
+    }
+
+    /**
+     * Registers a listener for the property of the given name.
+     * The listener will be notified every time that the property of the given name got a
new value.
+     * If the same listener is registered twice for the same property, then it will be notified
twice
+     * (this method does not perform duplication checks).
+     *
+     * @param  propertyName  name of the property to listen (should be one of the {@code
*_PROPERTY} constants).
+     * @param  listener      property listener to register.
+     */
+    public final void addPropertyChangeListener(final String propertyName, final PropertyChangeListener
listener) {
+        ArgumentChecks.ensureNonEmpty("propertyName", propertyName);
+        ArgumentChecks.ensureNonNull("listener", listener);
+        if (listeners == null) {
+            listeners = new HashMap<>(4);       // Assume few properties will be listened.
+        }
+        final PropertyChangeListener[] oldList = listeners.get(propertyName);
+        final PropertyChangeListener[] newList;
+        final int n;
+        if (oldList != null) {
+            n = oldList.length;
+            newList = Arrays.copyOf(oldList, n+1);
+        } else {
+            n = 0;
+            newList = new PropertyChangeListener[1];
+        }
+        newList[n] = listener;
+        if (!listeners.replace(propertyName, oldList, newList)) {
+            // Opportunistic safety against some multi-threading misuse.
+            throw new ConcurrentModificationException();
+        }
+    }
+
+    /**
+     * Unregisters a property listener. The given {@code propertyName} should be the name
used during
+     * {@linkplain #addPropertyChangeListener(String, PropertyChangeListener) listener registration}.
+     * If the specified listener is not registered for the named property, then nothing happen.
+     * If the listener has been registered twice, then only one registration is removed
+     * (one registration will remain).
+     *
+     * @param  propertyName  name of the listened property.
+     * @param  listener      property listener to unregister.
+     */
+    public final void removePropertyChangeListener(final String propertyName, final PropertyChangeListener
listener) {
+        ArgumentChecks.ensureNonEmpty("propertyName", propertyName);
+        ArgumentChecks.ensureNonNull("listener", listener);
+        if (listeners != null) {
+            final PropertyChangeListener[] oldList = listeners.get(propertyName);
+            if (oldList != null) {
+                for (int i=oldList.length; --i >= 0;) {
+                    if (oldList[i] == listener) {
+                        if (oldList.length != 1) {
+                            final PropertyChangeListener[] newList = ArraysExt.remove(oldList,
i, 1);
+                            if (listeners.replace(propertyName, oldList, newList)) {
+                                return;
+                            }
+                        } else if (listeners.remove(propertyName, oldList)) {
+                            return;
+                        }
+                        // Opportunistic safety against some multi-threading misuse.
+                        throw new ConcurrentModificationException();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Notifies all registered listeners that a property of the given name changed its value.
+     * The {@linkplain PropertyChangeEvent#getSource() change event source} will be {@code
this}.
+     * It is caller responsibility to verify that the old and new values are different
+     * (this method does not check for equality).
+     *
+     * @param  propertyName  name of the property that changed its value.
+     * @param  oldValue      the old property value (may be {@code null}).
+     * @param  newValue      the new property value (may be {@code null}).
+     *
+     * @see PropertyChangeEvent
+     * @see PropertyChangeListener
+     */
+    protected void firePropertyChange(final String propertyName, final Object oldValue, final
Object newValue) {
+        if (listeners != null) {
+            final PropertyChangeListener[] list = listeners.get(propertyName);
+            if (list != null) {
+                final PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName,
oldValue, newValue);
+                for (final PropertyChangeListener listener : list) {
+                    listener.propertyChange(event);
+                }
+            }
+        }
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
index c1b0e29..e1b88fe 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
@@ -17,22 +17,23 @@
 package org.apache.sis.internal.map;
 
 import org.opengis.feature.Feature;
-import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.DataStore;
 import org.apache.sis.coverage.grid.GridCoverage;
 
 
 /**
  * Parent class of all elements having a graphical representation on the map.
- * {@code Presentation} instances are organized in a tree parallel to the {@link MapLayer}
tree.
- * The {@link MapLayer} tree links data and styles in a device-independent way, while
- * the {@code Presentation} tree can be seen as map layers information "compiled"
- * in a form more directly exploitable by the display device.
- * In particular a {@code Presentation} objects must encapsulate data without
- * costly evaluation, processing or loading work remaining to be done.
- * (for example {@link Feature} or {@link GridCoverage} instances instead than {@link Resource}s).
+ * {@code Presentation} instances are organized in a tree closely related to the {@link MapLayer}
tree.
+ * The {@link MapLayer} tree specifies data and styles in a device-independent way and for
all zoom levels.
+ * The {@code Presentation} tree can be seen as {@link MapLayer} information filtered for
the current rendering
+ * context (map projection, zoom level, window size, <i>etc.</i>) and converted
to data structures more directly
+ * exploitable by the display device. In particular a {@code Presentation} object must encapsulate
data without
+ * costly evaluation, processing or loading work remaining to do: the {@link Feature} or
the {@link GridCoverage}
+ * (for instance) should have been read in advance from the {@link DataStore}.
+ * The preparation of a {@link Presentation} tree before displaying may be done in a background
thread.
  *
  * <p>Note that multiple presentations may be generated for the same feature.
- * Consequently many {@code Presentation} instances may encapsulate the same {@link Feature}
instances.</p>
+ * Consequently many {@code Presentation} instances may encapsulate the same {@link Feature}
instance.</p>
  *
  * <p>
  * NOTE: this class is a first draft subject to modifications.


Mime
View raw message