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 <[email protected]>
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.