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 480c551 Start writting the actual code for Canvas (not yet completed).
480c551 is described below
commit 480c551bcb66504352d35e2ee5220ee1167ccce2
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Feb 8 18:00:05 2020 +0100
Start writting the actual code for Canvas (not yet completed).
---
.../java/org/apache/sis/internal/map/Canvas.java | 503 ++++++++++++++++++---
.../org/apache/sis/internal/map/GridCanvas.java | 1 +
.../org/apache/sis/internal/map/PlanarCanvas.java | 125 ++++-
.../org/apache/sis/internal/map/package-info.java | 2 +-
.../org/apache/sis/geometry/GeneralEnvelope.java | 2 +-
.../operation/matrix/AffineTransforms2D.java | 2 +-
6 files changed, 546 insertions(+), 89 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
index 8f5bbf6..0c43c7c 100644
--- 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
@@ -16,18 +16,38 @@
*/
package org.apache.sis.internal.map;
+import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.geometry.MismatchedReferenceSystemException;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.opengis.metadata.spatial.DimensionNameType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.EngineeringCRS;
-import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.util.FactoryException;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.Localized;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.measure.Units;
-import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
+import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
+import org.apache.sis.internal.referencing.CoordinateOperations;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
/**
@@ -41,13 +61,13 @@ import org.apache.sis.util.ArgumentChecks;
* The contents are usually symbols, features or images, but some
implementations can also manage non-geographic
* elements like a map scale.</p>
*
- * <p>In addition of the set of {@link MapLayer} to display,
+ * <p>In addition to the set of {@link MapLayer}s to display,
* a {@code Canvas} manages four fundamental properties:</p>
* <ul>
* <li>The coordinate reference system to use for displaying data.</li>
* <li>The location of data to display in all dimensions, including the
dimensions
* not shown by the display device (for example time).</li>
- * <li>The size of the display device, in units of the display coordinate
system.</li>
+ * <li>The size of the display device, in units of the display coordinate
system (typically pixels).</li>
* <li>The conversion from the Coordinate Reference System to the display
coordinate system.</li>
* </ul>
*
@@ -68,23 +88,29 @@ import org.apache.sis.util.ArgumentChecks;
* 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>display 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.
+ * The {@link CRS#suggestCommonTarget CRS.suggestCommonTarget(…)} method
may be helpful
+ * for choosing an objective CRS from a set of data CRS.</li>
+ * <li>The {@linkplain #getDisplayCRS display CRS} is the coordinate system
of the display device.
+ * The {@linkplain #getObjectiveToDisplay() conversion from objective
CRS to display CRS}
+ * should be an affine transform with a scale, a translation and
optionally a rotation.
* This conversion changes every time that the user zooms or scrolls on
viewed data.</li>
* </ol>
*
* <h2>Location of data to display</h2>
* In addition of above-cited Coordinate Reference Systems, a {@code Canvas}
contains also a point of interest.
* The point of interest is often, but not necessarily, at the center of
display area.
- * It can be expressed in any CRS; it does not need to be the objective CRS or
the CRS of any data.
- * However the point of interest CRS must have enough dimensions for being
convertible to the CRS of all data.
- * In other words the number of dimensions of the point of interest is equal
or greater than the highest
- * number of dimensions found in data. The point of interest is used not only
for defining which point to show
- * in (typically) the center of the display area, but also for defining which
slice to select in all dimensions
- * not shown by the display device.
+ * It defines the position where {@linkplain #getResolution() resolutions}
will be computed, and where
+ * {@linkplain PlanarCanvas#scale(double, double) scales},
+ * {@linkplain PlanarCanvas#translate(double, double) translations} and
+ * {@linkplain PlanarCanvas#rotate(double) rotations} will be applied.
+ *
+ * <p>The point of interest can be expressed in any CRS;
+ * it does not need to be the objective CRS or the CRS of any data.
+ * However the CRS of that point must have enough dimensions for being
convertible to the CRS of all data.
+ * This rule implies that the number of dimensions of the point of interest is
equal or greater than
+ * the highest number of dimensions found in data. The purpose is not only to
specify which point to show in
+ * (typically) the center of the display area, but also to specify which slice
to select in all dimensions
+ * not shown by the display device.</p>
*
* <div class="note"><b>Example:</b> if some data have
(<var>x</var>,<var>y</var>,<var>z</var>) dimensions and
* other data have (<var>x</var>,<var>y</var>,<var>t</var>) dimensions, then
the point of interest shall contain
@@ -97,16 +123,29 @@ import org.apache.sis.util.ArgumentChecks;
* <h2>Display device size</h2>
* The geographic extent of data to be rendered is constrained by the zoom
level and the display device size.
* The display size is given by {@link #getDisplayBounds()} as an envelope
having the number of dimensions of
- * the display device. The zoom level is given indirectly by the {@link
#getObjectiveToDisplay()} transform.
+ * the display device. The display bounds is usually given in {@linkplain
Units#PIXEL pixel units}, but other
+ * units such as {@link Units#POINT} are also authorized.
+ * The zoom level is given indirectly by the {@link #getObjectiveToDisplay()}
transform.
* The display device may have a wraparound axis, for example in the spherical
coordinate system of a planetarium.
*
+ * <h2>Multi-threading</h2>
+ * {@code Canvas} is not thread-safe. Synchronization, if desired, must be
done by the caller.
+ *
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
-public abstract class Canvas extends Observable {
+public class Canvas extends Observable implements Localized {
+ /**
+ * Desired resolution in display units (usually pixels). This is used for
avoiding
+ * the cost of transformations having too much accuracy for the current
zoom level.
+ *
+ * @see #findTransform(CoordinateReferenceSystem,
CoordinateReferenceSystem)
+ */
+ private static final double DISPLAY_RESOLUTION = 1;
+
/**
* The {@value} property name, used for notifications about changes in
objective CRS.
* The objective CRS is the Coordinate Reference System in which all data
are transformed before displaying.
@@ -120,17 +159,15 @@ public abstract class Canvas extends Observable {
public static final String OBJECTIVE_CRS_PROPERTY = "objectiveCRS";
/**
- * The {@value} property name, used for notifications about changes in
point of interest.
- * The point of interest defines the location to show typically (but not
necessarily) in
- * the center of the display device. But it defines also the slice
coordinate values
- * in all dimensions beyond the ones shown by the device.
- * Associated values are instances of {@link DirectPosition}.
+ * The {@value} property name, used for notifications about changes in
<cite>objective to display</cite> conversion.
+ * This conversion maps coordinates in the {@linkplain #getObjectiveCRS()
objective CRS} to coordinates in the
+ * {@linkplain #getDisplayCRS() display CRS}. Associated values are
instances of {@link LinearTransform}.
*
- * @see #getPointOfInterest()
- * @see #setPointOfInterest(DirectPosition)
+ * @see #getObjectiveToDisplay()
+ * @see #setObjectiveToDisplay(LinearTransform)
* @see #addPropertyChangeListener(String, PropertyChangeListener)
*/
- public static final String POINT_OF_INTEREST_PROPERTY = "pointOfInterest";
+ public static final String OBJECTIVE_TO_DISPLAY_PROPERTY =
"objectiveToDisplay";
/**
* The {@value} property name, used for notifications about changes in
bounds of display device.
@@ -144,19 +181,55 @@ public abstract class Canvas extends Observable {
public static final String DISPLAY_BOUNDS_PROPERTY = "displayBounds";
/**
+ * The {@value} property name, used for notifications about changes in
point of interest.
+ * The point of interest defines the location to show typically (but not
necessarily) in
+ * the center of the display device. But it defines also the slice
coordinate values
+ * in all dimensions beyond the ones shown by the device.
+ * Associated values are instances of {@link DirectPosition}.
+ *
+ * @see #getPointOfInterest()
+ * @see #setPointOfInterest(DirectPosition)
+ * @see #addPropertyChangeListener(String, PropertyChangeListener)
+ */
+ public static final String POINT_OF_INTEREST_PROPERTY = "pointOfInterest";
+
+ /**
* The coordinate reference system in which to transform all data before
displaying.
* If {@code null}, then no transformation is applied and data coordinates
are used directly
* as display coordinates, regardless the data CRS (even if different data
use different CRS).
*
* @see #OBJECTIVE_CRS_PROPERTY
* @see #getObjectiveCRS()
+ * @see #setObjectiveCRS(CoordinateReferenceSystem)
*/
private CoordinateReferenceSystem objectiveCRS;
/**
+ * The conversion from {@linkplain #getObjectiveCRS() objective CRS} to
the display coordinate system.
+ * Conceptually this conversion should never be null (its initial value is
the identity conversion).
+ * However subclasses may use a more specialized type such as {@link
java.awt.geom.AffineTransform}
+ * and set this field to {@code null} for recomputing it from the
specialized type when requested.
+ *
+ * @see #OBJECTIVE_TO_DISPLAY_PROPERTY
+ * @see #getObjectiveToDisplay()
+ * @see #setObjectiveToDisplay(LinearTransform)
+ */
+ private LinearTransform objectiveToDisplay;
+
+ /**
+ * The size and location of the output device, modified in-place if the
size change.
+ * The CRS of this envelope is the display CRS. Coordinate values are
initially NaN.
+ *
+ * @see #DISPLAY_BOUNDS_PROPERTY
+ * @see #getDisplayBounds()
+ * @see #getDisplayCRS()
+ */
+ final GeneralEnvelope displayBounds;
+
+ /**
* The point of interest to show typically (but not necessarily) in the
center of display area.
* Also used for selecting a slice in all supplemental dimensions.
- * If {@code null}, then the 0 coordinate value is assumed in all
dimensions.
+ * If {@code null}, then calculations that depend on a point of interest
are skipped.
*
* @see #POINT_OF_INTEREST_PROPERTY
* @see #getPointOfInterest()
@@ -164,30 +237,71 @@ public abstract class Canvas extends Observable {
private GeneralDirectPosition pointOfInterest;
/**
- * The size and location of the output device, modified in-place if the
size change.
- * The CRS of this envelope is the display CRS.
+ * The point of interest transformed to the objective CRS, or {@code null}
if {@link #pointOfInterest}
+ * has not yet been provided. This point shall be updated immediately when
{@link #pointOfInterest} is
+ * updated, as a way to verify that the point is valid.
+ */
+ private DirectPosition objectivePOI;
+
+ /**
+ * The factory to use for creating coordinate operations. This factory
allow us to specify the area
+ * of interest (the geographic region shown by this {@code Canvas}) and
the desired resolution.
*
- * @see #DISPLAY_BOUNDS_PROPERTY
- * @see #getDisplayBounds()
- * @see #getDisplayCRS()
+ * @see #findTransform(CoordinateReferenceSystem,
CoordinateReferenceSystem)
*/
- private final GeneralEnvelope displayBounds;
+ private final DefaultCoordinateOperationFactory coordinateOperationFactory;
/**
- * Creates a new canvas for an output device using the given coordinate
reference system.
+ * The locale for labels or error messages.
+ */
+ private final Locale locale;
+
+ /**
+ * Creates a new canvas for a display device using the given coordinate
reference system.
* The display CRS of a canvas can not be changed after construction.
* Its coordinate system depends on the display device shape
* (for example a two-dimensional Cartesian coordinate system for flat
screens,
- * or a spherical coordinate system for planetarium domes).
+ * or a polar or spherical coordinate system for planetarium domes).
* The axis units of measurement are typically (but not necessarily)
{@link Units#PIXEL}
* for Cartesian coordinate systems, with {@link Units#DEGREE} in polar,
cylindrical or
* spherical coordinate systems.
*
* @param displayCRS the coordinate system of the display device.
+ * @param locale the locale to use for labels and some messages, or
{@code null} for default.
*/
- protected Canvas(final EngineeringCRS displayCRS) {
+ protected Canvas(final EngineeringCRS displayCRS, final Locale locale) {
+ this.locale = locale;
ArgumentChecks.ensureNonNull("displayCRS", displayCRS);
displayBounds = new GeneralEnvelope(displayCRS);
+ displayBounds.setToNaN();
+ coordinateOperationFactory = CoordinateOperations.factory();
+ }
+
+ /**
+ * Returns the locale used for texts or for producing some error messages.
+ * May be {@code null} if no locale has been specified, in which case
+ * the {@linkplain Locale#getDefault() system default} should be used.
+ *
+ * @return the locale for messages, or {@code null} if not explicitly
defined.
+ */
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * Returns the number of dimensions of the display device.
+ * Subclasses may override for a little bit more efficiency.
+ */
+ int getDisplayDimensions() {
+ return ReferencingUtilities.getDimension(getDisplayCRS());
+ }
+
+ /**
+ * Returns name of display axes, or {@code null} if unknown.
+ */
+ DimensionNameType[] getDisplayAxes() {
+ return null;
}
/**
@@ -197,11 +311,17 @@ public abstract class Canvas extends Observable {
* spherical coordinate systems. The coordinate system may have a
wraparound axis for
* some "exotic" display devices (e.g. planetarium dome).
*
- * <div class="note"><b>Note:</b> invoking this method is rarely needed.
It is sufficient to
- * said that a display CRS exists at least conceptually, and that we
define a conversion from
- * the objective CRS to that display CRS.</div>
+ * <div class="note"><b>Usage note:</b> invoking this method is rarely
needed. It is sufficient
+ * to said that a display CRS exists at least conceptually, and that we
define a conversion from
+ * the objective CRS to that display CRS. This method may be useful when
the subclasses may be
+ * something else than {@link PlanarCanvas}, in which case the caller may
want more information
+ * about the geometry of the display device.</div>
*
- * @return the coordinate reference system of the display device.
+ * <p>Note that the {@link CRS#findOperation CRS.findOperation(…)} static
method can generally
+ * not handle this display CRS. To apply coordinate operations on display
coordinates,
+ * {@link #getObjectiveToDisplay()} transform must be inverted and
used.</p>
+ *
+ * @return the Coordinate Reference System of the display device.
*
* @see #getObjectiveCRS()
* @see #getObjectiveToDisplay()
@@ -213,7 +333,7 @@ public abstract class Canvas extends Observable {
/**
* Returns the Coordinate Reference System in which all data are
transformed before displaying.
* After conversion to this CRS, coordinates should be related to the
display device coordinates
- * with only a final scale, a translation and optionally a rotation to add.
+ * with only a final scale, a translation and optionally a rotation
remaining to apply.
*
* <p>This value may be {@code null} on newly created {@code Canvas},
before data are added and canvas
* is configured. It should not be {@code null} anymore once a {@code
Canvas} is ready for displaying.</p>
@@ -230,12 +350,20 @@ public abstract class Canvas extends Observable {
/**
* Sets the Coordinate Reference System in which all data are transformed
before displaying.
+ * The given CRS should have a domain of validity wide enough for
encompassing all data
+ * (the {@link CRS#suggestCommonTarget CRS.suggestCommonTarget(…)} method
may be helpful
+ * for choosing an objective CRS from a set of data CRS).
* 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.
*
- * <p>The domain of validity of the given CRS 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.</p>
+ * <p>If the transform between old and new CRS is not identity, then this
method recomputes
+ * the <cite>objective to display</cite> conversion in a way preserving
the display coordinates
+ * of the {@link #getPointOfInterest() point of interest}, together with
the scales, shapes and
+ * orientations of features in close neighborhood of that point.
+ * This calculation may cause {@value #OBJECTIVE_TO_DISPLAY_PROPERTY}
property change event
+ * to be sent to listeners, in addition of above-cited {@value
#OBJECTIVE_CRS_PROPERTY}
+ * (note that {@value #POINT_OF_INTEREST_PROPERTY} stay unchanged).
+ * All those change events are sent only after all property values have
been updated to their new values.</p>
*
* @param newValue the new Coordinate Reference System in which to
transform all data before displaying.
* @throws NullPointerException if the given CRS is null.
@@ -244,11 +372,180 @@ public abstract class Canvas extends Observable {
*/
public void setObjectiveCRS(final CoordinateReferenceSystem newValue)
throws RenderException {
ArgumentChecks.ensureNonNull(OBJECTIVE_CRS_PROPERTY, newValue);
- ArgumentChecks.ensureDimensionMatches(OBJECTIVE_CRS_PROPERTY,
getObjectiveToDisplay().getSourceDimensions(), newValue);
+ ArgumentChecks.ensureDimensionMatches(OBJECTIVE_CRS_PROPERTY,
getDisplayDimensions(), newValue);
final CoordinateReferenceSystem oldValue = objectiveCRS;
+ LinearTransform oldObjectiveToDisplay = null;
+ LinearTransform newObjectiveToDisplay = null;
if (!Objects.equals(oldValue, newValue)) {
- objectiveCRS = newValue;
+ if (oldValue != null) try {
+ /*
+ * Compute the change unconditionally as a way to verify that
the new CRS is compatible with
+ * data currently shown. Another reason is that checking
identity transform is more reliable
+ * than the `compareIgnoreMetadata(oldValue, newValue)` check.
+ */
+ final MathTransform newToOld = findTransform(newValue,
oldValue);
+ if (pointOfInterest != null && !newToOld.isIdentity()) {
+ oldObjectiveToDisplay = getObjectiveToDisplay();
+ /*
+ * Conceptually, we want the coordinates in new CRS to be
as they were in the old CRS
+ * (same location, same Jacobian matrix) in the
neighborhood of the point of interest,
+ * so that we can apply the old `objectiveToCRS`
transform. For achieving that goal,
+ * we apply a local affine transform which cancel the
effect of "old CRS → new CRS"
+ * transformation around the point of interest. The effect
of CRS change will appear
+ * as we look further from the point of interest.
+ */
+ final MathTransform poiToNew =
findTransform(pointOfInterest.getCoordinateReferenceSystem(), newValue);
+ final DirectPosition poiInNew =
poiToNew.transform(pointOfInterest, allocatePosition());
+ final LinearTransform cancel =
MathTransforms.tangent(newToOld, poiInNew);
+ final MathTransform result =
MathTransforms.concatenate(cancel, oldObjectiveToDisplay);
+ /*
+ * The result is the new `objectiveToTransform` such as
the display is unchanged around POI.
+ * That transform should be an instance of
`LinearTransform` because the two concatenated
+ * transforms were linear, but we nevertheless invoke
`tangent(…)` again as a safety;
+ * normally it should just return the `result` as-is.
+ */
+ newObjectiveToDisplay = MathTransforms.tangent(result,
poiInNew);
+ updateObjectiveToDisplay(newObjectiveToDisplay);
+ objectivePOI = poiInNew; // Set only after everything
else succeeded.
+ }
+ } catch (FactoryException | TransformException e) {
+ throw new
RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
OBJECTIVE_CRS_PROPERTY), e);
+ }
+ objectiveCRS = newValue; // Set only after everything
else succeeded.
firePropertyChange(OBJECTIVE_CRS_PROPERTY, oldValue, newValue);
+ if (!Objects.equals(oldObjectiveToDisplay, newObjectiveToDisplay))
{
+ firePropertyChange(OBJECTIVE_TO_DISPLAY_PROPERTY,
oldObjectiveToDisplay, newObjectiveToDisplay);
+ }
+ }
+ }
+
+ /**
+ * Returns the (usually affine) conversion from objective CRS to display
coordinate system.
+ * The source coordinates shall be in the CRS given by {@link
#getObjectiveCRS()} and the
+ * converted coordinates will be in the CRS given by {@link
#getDisplayCRS()}.
+ *
+ * <p>The <cite>objective to display</cite> conversion changes every time
that user zooms
+ * or scrolls on viewed data. However the transform returned by this
method is a snapshot
+ * taken at the time this method is invoked; subsequent changes in the
<cite>objective to
+ * display</cite> conversion are not reflected in the returned
transform.</p>
+ *
+ * @return snapshot of the (usually affine) conversion from objective CRS
+ * to display coordinate system (never {@code null}).
+ *
+ * @see #OBJECTIVE_CRS_PROPERTY
+ * @see #getObjectiveCRS()
+ * @see #getDisplayCRS()
+ */
+ public LinearTransform getObjectiveToDisplay() {
+ if (objectiveToDisplay == null) {
+ objectiveToDisplay = updateObjectiveToDisplay();
+ }
+ return objectiveToDisplay;
+ }
+
+ /**
+ * Takes a snapshot of the <cite>objective to display</cite> conversion.
This method needs
+ * to be overridden only by subclasses that use their own specialized
class instead than
+ * {@link #objectiveToDisplay} for managing changes in the zooms or viewed
area.
+ *
+ * @see #updateObjectiveToDisplay(LinearTransform)
+ */
+ LinearTransform updateObjectiveToDisplay() {
+ return MathTransforms.identity(getDisplayDimensions());
+ }
+
+ /**
+ * Sets the conversion from objective CRS to display coordinate system.
+ * If the given value is different than the previous value, then a change
event is sent
+ * to all listeners registered for the {@value
#OBJECTIVE_TO_DISPLAY_PROPERTY} property.
+ *
+ * <p>Invoking this method has the effect of changing the viewed area, the
zoom level or the rotation of the map.
+ * It does not update the {@value #POINT_OF_INTEREST_PROPERTY} property
however. The point of interest may move
+ * outside the view area as a result of this method call.</p>
+ *
+ * @param newValue the new <cite>objective to display</cite> conversion.
+ * @throws IllegalArgumentException if given the transform does not have
the expected number of dimensions or is not affine.
+ * @throws RenderException if the <cite>objective to display</cite>
transform can not be set to the given value for another reason.
+ */
+ public void setObjectiveToDisplay(LinearTransform newValue) throws
RenderException {
+ ArgumentChecks.ensureNonNull(OBJECTIVE_TO_DISPLAY_PROPERTY, newValue);
+ final int expected = getDisplayDimensions();
+ int actual = newValue.getSourceDimensions();
+ if (actual == expected) {
+ actual = newValue.getTargetDimensions();
+ if (actual == expected) {
+ LinearTransform oldValue = objectiveToDisplay; // Do not
invoke user-overridable method.
+ if (oldValue == null) {
+ oldValue = updateObjectiveToDisplay();
+ }
+ if (!Objects.equals(oldValue, newValue)) {
+ updateObjectiveToDisplay(newValue);
+ firePropertyChange(OBJECTIVE_TO_DISPLAY_PROPERTY,
oldValue, newValue);
+ }
+ return;
+ }
+ }
+ throw new MismatchedDimensionException(errors().getString(
+ Errors.Keys.MismatchedDimension_3,
OBJECTIVE_TO_DISPLAY_PROPERTY, expected, actual));
+ }
+
+ /**
+ * Sets the conversion from objective CRS to display coordinate system.
+ * Contrarily to other setter methods, this method does not notify
listeners about that change;
+ * it is caller responsibility to send a {@value
#OBJECTIVE_TO_DISPLAY_PROPERTY} change event.
+ * This design choice is because this method is usually invoked as part of
a larger set of changes.
+ *
+ * @see #updateObjectiveToDisplay()
+ */
+ void updateObjectiveToDisplay(final LinearTransform newValue) {
+ objectiveToDisplay = newValue;
+ }
+
+ /**
+ * Returns the size and location of the display device.
+ * The unit of measurement is typically (but not necessarily) pixels.
+ * The coordinate values are often integers, but this is not mandatory.
+ * The coordinate reference system is given by {@link #getDisplayCRS()}.
+ *
+ * <p>This value may be {@code null} on newly created {@code Canvas},
before data are added and canvas
+ * is configured. It should not be {@code null} anymore once a {@code
Canvas} is ready for displaying.</p>
+ *
+ * @return size and location of the display device.
+ *
+ * @see #DISPLAY_BOUNDS_PROPERTY
+ */
+ public Envelope getDisplayBounds() {
+ return displayBounds.isAllNaN() ? null : new
GeneralEnvelope(displayBounds);
+ }
+
+ /**
+ * Sets the size and location of the display device. The envelope CRS
shall be either the
+ * {@linkplain #getDisplayCRS() display CRS} or unspecified, in which case
the display CRS
+ * is assumed. Unit of measurement is typically (but not necessarily)
{@link Units#PIXEL}.
+ * If the given value is different than the previous value, then a change
event is sent to
+ * all listeners registered for the {@value #DISPLAY_BOUNDS_PROPERTY}
property.
+ *
+ * @param newValue the new display bounds.
+ * @throws IllegalArgumentException if the given envelope does not have
the expected CRS or number of dimensions.
+ * @throws RenderException if the display bounds can not be set to the
given value for another reason.
+ */
+ public void setDisplayBounds(final Envelope newValue) throws
RenderException {
+ ArgumentChecks.ensureNonNull(DISPLAY_BOUNDS_PROPERTY, newValue);
+ if (objectiveCRS != null) {
+ final CoordinateReferenceSystem crs =
newValue.getCoordinateReferenceSystem();
+ if (crs != null && !Utilities.equalsIgnoreMetadata(objectiveCRS,
crs)) {
+ throw new
MismatchedReferenceSystemException(errors().getString(
+ Errors.Keys.IllegalCoordinateSystem_1,
IdentifiedObjects.getDisplayName(crs, getLocale())));
+ }
+ }
+ final GeneralEnvelope oldValue = new GeneralEnvelope(displayBounds);
+ displayBounds.setEnvelope(newValue);
+ if (displayBounds.isEmpty()) {
+ displayBounds.setEnvelope(oldValue);
+ throw new
IllegalArgumentException(errors().getString(Errors.Keys.EmptyProperty_1,
DISPLAY_BOUNDS_PROPERTY));
+ }
+ if (!Objects.equals(oldValue, displayBounds)) {
+ firePropertyChange(DISPLAY_BOUNDS_PROPERTY, oldValue, newValue);
// Do not publish reference to `displayBounds`.
}
}
@@ -273,8 +570,8 @@ public abstract class Canvas extends Observable {
/**
* Sets the coordinates of the point to show typically (but not
necessarily) in the center of display area.
- * If the given value is different than the previous value, then a change
event
- * is sent to all listeners registered for the {@value
#POINT_OF_INTEREST_PROPERTY} property.
+ * If the given value is different than the previous value, then a change
event is sent to all listeners
+ * registered for the {@value #POINT_OF_INTEREST_PROPERTY} property.
*
* @param newValue the new coordinates of the point to show typically in
the center of display area.
* @throws NullPointerException if the given position is null.
@@ -284,37 +581,107 @@ public abstract class Canvas extends Observable {
ArgumentChecks.ensureNonNull(POINT_OF_INTEREST_PROPERTY, newValue);
final GeneralDirectPosition copy = new GeneralDirectPosition(newValue);
final GeneralDirectPosition oldValue = pointOfInterest;
- if (!Objects.equals(oldValue, copy)) {
+ if (!Objects.equals(oldValue, copy)) try {
+ final MathTransform mt =
findTransform(newValue.getCoordinateReferenceSystem(), objectiveCRS);
+ objectivePOI = mt.transform(copy, allocatePosition());
pointOfInterest = copy;
- firePropertyChange(POINT_OF_INTEREST_PROPERTY, oldValue,
newValue); // Really `newValue`, not `copy`.
+ firePropertyChange(POINT_OF_INTEREST_PROPERTY, oldValue,
newValue); // Do not publish reference to `copy`.
+ } catch (FactoryException | TransformException e) {
+ throw new
RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
POINT_OF_INTEREST_PROPERTY), e);
}
}
/**
- * Returns the size and location of the display device.
+ * Returns canvas properties (objective CRS, display bounds, conversion)
encapsulated in a grid geometry.
+ * This is a convenience method for interoperability with grid coverage
API. Properties are mapped as below:
*
- * @return size and location of the display device.
+ * <table>
+ * <caption>Canvas to grid geometry properties</caption>
+ * <tr>
+ * <th>Grid geometry value</th>
+ * <th>Canvas value</th>
+ * </tr><tr>
+ * <td>{@link GridGeometry#getCoordinateReferenceSystem()}</td>
+ * <td>{@link #getObjectiveCRS()}</td>
+ * </tr><tr>
+ * <td>{@link GridGeometry#getExtent()}</td>
+ * <td>{@link #getDisplayBounds()} rounded to enclosing integers</td>
+ * </tr><tr>
+ * <td>{@link GridGeometry#getGridToCRS(PixelInCell)}</td>
+ * <td>Inverse of {@link #getObjectiveToDisplay()}</td>
+ * </tr>
+ * </table>
+ *
+ * @param allDimensions if {@code true}, all dimensions from the point
of interest are included in
+ * the returned grid geometry. If {@code false}, only the
displayed dimensions are included.
+ * @return a grid geometry encapsulating canvas properties.
+ * @throws RenderException if the grid geometry can not be computed.
*/
- public Envelope getDisplayBounds() {
- return new ImmutableEnvelope(displayBounds);
+ public GridGeometry getGridGeometry(final boolean allDimensions) throws
RenderException {
+ final GridExtent extent;
+ if (displayBounds.isEmpty()) {
+ extent = null;
+ } else {
+ // TODO: take allDimensions in account.
+ final int dimension = displayBounds.getDimension();
+ final long[] lower = new long[dimension];
+ final long[] upper = new long[dimension];
+ for (int i=0; i<dimension; i++) {
+ lower[i] = (long) Math.floor(displayBounds.getMinimum(i));
+ upper[i] = (long) Math.ceil (displayBounds.getMaximum(i));
+ }
+ final DimensionNameType[] axisTypes = getDisplayAxes();
+ extent = new GridExtent(axisTypes, lower, upper, false);
+ }
+ try {
+ // TODO: take allDimensions in account.
+ return new GridGeometry(extent, PixelInCell.CELL_CENTER,
objectiveToDisplay.inverse(), objectiveCRS);
+ } catch (TransformException e) {
+ throw new
RenderException(errors().getString(Errors.Keys.CanNotCompute_1,
POINT_OF_INTEREST_PROPERTY), e);
+ }
}
- public void setDisplayBounds(final Envelope newValue) {
- ArgumentChecks.ensureNonNull(DISPLAY_BOUNDS_PROPERTY, newValue);
- displayBounds.setEnvelope(newValue);
+ public void setGridGeometry(final GridGeometry geometry) throws
RenderException {
+ // TODO
+ }
+
+ public Optional<GeographicBoundingBox> getGeographicArea() {
+ return Optional.empty(); // TODO
+ }
+
+ public double[] getResolution() {
+ return null;
}
/**
- * Returns the (usually affine) conversion from objective CRS to display
coordinate system.
- * The number of source dimensions shall be the number of dimensions of
the {@linkplain #getObjectiveCRS() objective CRS}.
- * The number of target dimensions shall be the number of dimensions of
the display device.
- * That conversion will change every time that the user zooms or scrolls
on viewed data.
- * This method shall never return {@code null}.
- *
- * @return conversion (usually affine) from objective CRS to display
coordinate system.
- *
- * @see #getObjectiveCRS()
- * @see #getDisplayCRS()
+ * Allocates a position which can hold a coordinates in objective or
display CRS, or
+ * returns {@code null} for letting {@link MathTransform} do the
allocation themselves.
+ * May be overridden by subclasses for a little bit more efficiency.
*/
- public abstract LinearTransform getObjectiveToDisplay();
+ DirectPosition allocatePosition() {
+ return null;
+ }
+
+ /**
+ * Returns the transform from the given source CRS to the given target CRS
with precedence for an operation
+ * valid for the geographic area of this canvas. The transform returned by
this method for the same pair of
+ * CRS may differ depending on which area is currently visible in the
canvas. All requests for a coordinate
+ * operation should invoke this method instead than {@link
CRS#findOperation(CoordinateReferenceSystem,
+ * CoordinateReferenceSystem, GeographicBoundingBox)}.
+ */
+ private MathTransform findTransform(final CoordinateReferenceSystem source,
+ final CoordinateReferenceSystem
target) throws FactoryException
+ {
+ final CoordinateOperationContext context = new
CoordinateOperationContext();
+ final Optional<GeographicBoundingBox> geographicArea =
getGeographicArea();
+ geographicArea.ifPresent(context::setAreaOfInterest);
+ return coordinateOperationFactory.createOperation(source, target,
context).getMathTransform();
+ }
+
+ /**
+ * Returns the resources bundle for error messages in the locale of this
canvas.
+ */
+ private Errors errors() {
+ return Errors.getResources(locale);
+ }
}
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 66c1e4a..32369e4 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
@@ -70,6 +70,7 @@ public abstract class GridCanvas extends PlanarCanvas {
private boolean proportion = true;
protected GridCanvas() {
+ super(null);
}
public GridGeometry getGridGeometry() {
diff --git
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
index 54a3b83..68e6c8f 100644
---
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
+++
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PlanarCanvas.java
@@ -17,13 +17,20 @@
package org.apache.sis.internal.map;
import java.util.Map;
+import java.util.Locale;
import java.awt.geom.AffineTransform;
+import org.opengis.geometry.Envelope;
+import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.metadata.spatial.DimensionNameType;
import org.apache.sis.measure.Units;
+import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.referencing.cs.DefaultCartesianCS;
import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
import org.apache.sis.referencing.datum.DefaultEngineeringDatum;
import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
@@ -43,6 +50,19 @@ import static
org.opengis.referencing.IdentifiedObject.NAME_KEY;
*/
public abstract class PlanarCanvas extends Canvas {
/**
+ * The {@value} constant for identifying code specific to bi-dimensional
case.
+ */
+ private static final int BIDIMENSIONAL = 2;
+
+ /**
+ * Name of grid axes in {@link org.apache.sis.coverage.grid.GridGeometry}
extent.
+ */
+ private static final DimensionNameType[] DISPLAY_AXES = {
+ DimensionNameType.COLUMN,
+ DimensionNameType.ROW
+ };
+
+ /**
* The display Coordinate Reference System used by all {@code
PlanarCanvas} instances.
*/
private static final DefaultEngineeringCRS DISPLAY_CRS;
@@ -58,37 +78,106 @@ public abstract class PlanarCanvas extends Canvas {
/**
* The conversion from {@linkplain #getObjectiveCRS() objective CRS} to
the display coordinate system.
* This transform will be modified in-place when user applies zoom,
translation or rotation on the view area.
+ * The {@link #objectiveToDisplay} transform inherited from parent class
is used as an immutable snapshot of
+ * this {@link #toDisplayAsAffine} transform. That snapshot is created
when needed and reset to {@code null}
+ * when {@link #toDisplayAsAffine} is modified.
+ *
+ * @see #getObjectiveToDisplay()
+ * @see #objectiveToDisplay
*/
- private final AffineTransform objectiveToDisplay;
+ private final AffineTransform toDisplayAsAffine;
/**
- * An immutable snapshot of {@link #objectiveToDisplay}, created when
needed.
- * This field is reset to {@code null} when {@link #objectiveToDisplay} is
modified.
+ * Creates a new two-dimensional canvas.
*
- * @see #getObjectiveToDisplay()
+ * @param locale the locale to use for labels and some messages, or
{@code null} for default.
+ */
+ protected PlanarCanvas(final Locale locale) {
+ super(DISPLAY_CRS, locale);
+ toDisplayAsAffine = new AffineTransform();
+ }
+
+ /**
+ * Returns the number of dimensions of the display device.
*/
- private AffineTransform2D conversionSnapshot;
+ @Override
+ final int getDisplayDimensions() {
+ return BIDIMENSIONAL;
+ }
/**
- * Creates a new two-dimensional canvas.
+ * Returns name of display axes, or {@code null} if unknown.
+ * Caller shall not modify the returned array (it is not cloned).
*/
- protected PlanarCanvas() {
- super(DISPLAY_CRS);
- objectiveToDisplay = new AffineTransform();
+ @Override
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ final DimensionNameType[] getDisplayAxes() {
+ return DISPLAY_AXES;
}
/**
- * Returns the conversion from objective CRS to display coordinate system.
- * The number of source and target dimensions is always 2.
- * That conversion will change every time that the user zooms or scrolls
on viewed data.
+ * Allocates a position which can hold a coordinates in objective or
display CRS.
+ */
+ @Override
+ final DirectPosition allocatePosition() {
+ return new DirectPosition2D();
+ }
+
+ /**
+ * Returns the size and location of the display device. The unit of
measurement is
+ * {@link Units#PIXEL} and coordinate values are usually (but not
necessarily) integers.
+ *
+ * <p>This value may be {@code null} on newly created {@code Canvas},
before data are added and canvas
+ * is configured. It should not be {@code null} anymore once a {@code
Canvas} is ready for displaying.</p>
+ *
+ * @return size and location of the display device.
*
- * @return conversion from objective CRS to display coordinate system.
+ * @see #setDisplayBounds(Envelope)
*/
@Override
- public LinearTransform getObjectiveToDisplay() {
- if (conversionSnapshot == null) {
- conversionSnapshot = new AffineTransform2D(objectiveToDisplay);
- }
- return conversionSnapshot;
+ public Envelope2D getDisplayBounds() {
+ return displayBounds.isAllNaN() ? null : new Envelope2D(displayBounds);
+ }
+
+ /**
+ * Returns the affine conversion from objective CRS to display coordinate
system.
+ * The transform returned by this method is a snapshot taken at the time
this method is invoked;
+ * subsequent changes in the <cite>objective to display</cite> conversion
are not reflected in
+ * the returned transform.
+ *
+ * @return snapshot of the affine conversion from objective CRS
+ * to display coordinate system (never {@code null}).
+ */
+ @Override
+ final LinearTransform updateObjectiveToDisplay() {
+ return new AffineTransform2D(toDisplayAsAffine);
+ }
+
+ /**
+ * Sets the conversion from objective CRS to display coordinate system.
+ * Contrarily to other setter methods, this method does not notify
listeners about that change;
+ * it is caller responsibility to send a {@value
#OBJECTIVE_TO_DISPLAY_PROPERTY} change event.
+ * This method does not update the {@value #POINT_OF_INTEREST_PROPERTY}
property;
+ * the point of interest may move outside the view area as a result of
this method call.
+ *
+ * @param newValue the new <cite>objective to display</cite> conversion.
+ * @throws IllegalArgumentException if the given transform is not
two-dimensional or is not affine.
+ */
+ @Override
+ final void updateObjectiveToDisplay(final LinearTransform newValue) {
+
toDisplayAsAffine.setTransform(AffineTransforms2D.castOrCopy(newValue.getMatrix()));
+ super.updateObjectiveToDisplay(newValue);
+ }
+
+ public void scale(final double sx, final double sy) {
+ // TODO
+ }
+
+ public void translate(final double tx, final double ty) {
+ // TODO
+ }
+
+ public void rotate(final double angle) {
+ // TODO
}
}
diff --git
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
index 324ac5a..431b304 100644
---
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
+++
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
@@ -25,7 +25,7 @@
* about their stability.</p>
*
* <h2>Synchronization</h2>
- * Unless otherwise specified, classes in this package are not thread safe.
+ * Unless otherwise specified, classes in this package are not thread-safe.
* Synchronization, if desired, must be done by the caller.
*
* @author Johann Sorel (Geomatys)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralEnvelope.java
b/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralEnvelope.java
index fb6e2da..b017c45 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralEnvelope.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/geometry/GeneralEnvelope.java
@@ -397,7 +397,7 @@ public class GeneralEnvelope extends ArrayEnvelope
implements Cloneable, Seriali
* the CRS of this envelope will be set to the CRS of the given envelope.
*
* @param envelope the envelope to copy coordinates from.
- * @throws MismatchedDimensionException if the specified envelope doesn't
have
+ * @throws MismatchedDimensionException if the specified envelope does not
have
* the expected number of dimensions.
*/
public void setEnvelope(final Envelope envelope) throws
MismatchedDimensionException {
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
index a3441d4..876b6a0 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java
@@ -67,7 +67,7 @@ public final class AffineTransforms2D extends Static {
}
MatrixSIS.ensureSizeMatch(3, 3, matrix);
if (!Matrices.isAffine(matrix)) {
- throw new
IllegalStateException(Resources.format(Resources.Keys.NotAnAffineTransform));
+ throw new
IllegalArgumentException(Resources.format(Resources.Keys.NotAnAffineTransform));
}
return new AffineTransform(matrix.getElement(0,0),
matrix.getElement(1,0),
matrix.getElement(0,1),
matrix.getElement(1,1),