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: Save an estimation of transformation accuracy in a new `org.apache.sis.PositionalAccuracy` image property.
Date Wed, 24 Jun 2020 14:51:42 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 473f792  Save an estimation of transformation accuracy in a new `org.apache.sis.PositionalAccuracy` image property.
473f792 is described below

commit 473f792ae33a08d7bad25770a8ce8e2d9c889c8b
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Wed Jun 24 16:50:44 2020 +0200

    Save an estimation of transformation accuracy in a new `org.apache.sis.PositionalAccuracy` image property.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |   6 +-
 .../org/apache/sis/gui/coverage/RenderingData.java |  30 ++---
 .../java/org/apache/sis/gui/map/MapCanvasAWT.java  |   2 +-
 .../coverage/grid/CoordinateOperationFinder.java   |  27 +++++
 .../sis/coverage/grid/GridCoverageProcessor.java   |  13 +-
 .../sis/coverage/grid/ResampledGridCoverage.java   |   5 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 123 ++++++++++++-------
 .../java/org/apache/sis/image/PlanarImage.java     |  21 ++++
 .../java/org/apache/sis/image/ResampledImage.java  | 133 +++++++++++++++------
 .../org/apache/sis/image/ResampledImageTest.java   |   2 +-
 .../java/org/apache/sis/measure/Quantities.java    |  76 +++++++++++-
 .../src/main/java/org/apache/sis/util/Numbers.java |  18 +++
 .../org/apache/sis/measure/QuantitiesTest.java     |  16 ++-
 13 files changed, 360 insertions(+), 112 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 01e6bbc..5a1f136 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -135,7 +135,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         resampledImages       = new EnumMap<>(Stretching.class);
         coverageProperty      = new SimpleObjectProperty<>(this, "coverage");
         sliceExtentProperty   = new SimpleObjectProperty<>(this, "sliceExtent");
-        interpolationProperty = new SimpleObjectProperty<>(this, "interpolation", data.getInterpolation());
+        interpolationProperty = new SimpleObjectProperty<>(this, "interpolation", data.processor.getInterpolation());
         coverageProperty     .addListener((p,o,n) -> onImageSpecified());
         sliceExtentProperty  .addListener((p,o,n) -> onImageSpecified());
         interpolationProperty.addListener((p,o,n) -> onInterpolationSpecified(n));
@@ -313,7 +313,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      * Invoked when a new interpolation has been specified.
      */
     private void onInterpolationSpecified(final Interpolation interpolation) {
-        data.setInterpolation(interpolation);
+        data.processor.setInterpolation(interpolation);
         resampledImages.clear();
         requestRepaint();
     }
@@ -447,7 +447,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         }
 
         /**
-         * Invoked in JavaFX thread after {@link #paint(Graphics2D)} completion.
+         * Invoked in JavaFX thread after successful {@link #paint(Graphics2D)} completion.
          * This method stores the computation results.
          */
         @Override
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index 84f9e12..6a9e97b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -36,12 +36,13 @@ import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
-import org.apache.sis.image.Interpolation;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.PreferredSize;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.math.Statistics;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -140,7 +141,7 @@ final class RenderingData implements Cloneable {
     /**
      * The processor that we use for resampling image and stretching their color ramps.
      */
-    private ImageProcessor processor;
+    final ImageProcessor processor;
 
     /**
      * Creates a new instance initialized to no image.
@@ -196,25 +197,6 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Gets the interpolation method to use during resample operations.
-     *
-     * @see CoverageCanvas#getInterpolation()
-     */
-    final Interpolation getInterpolation() {
-        return processor.getInterpolation();
-    }
-
-    /**
-     * Sets the interpolation method to use during resample operations.
-     *
-     * @see CoverageCanvas#setInterpolation(Interpolation)
-     */
-    final void setInterpolation(final Interpolation newValue) {
-        processor = processor.clone();          // Previous processor may be in use by background thread.
-        processor.setInterpolation(newValue);
-    }
-
-    /**
      * Creates the resampled image. This method will compute the {@link MathTransform} steps from image
      * coordinate system to display coordinate system if those steps have not already been computed.
      */
@@ -232,6 +214,9 @@ final class RenderingData implements Cloneable {
             }
             try {
                 changeOfCRS = CRS.findOperation(dataGeometry.getCoordinateReferenceSystem(), objectiveCRS, areaOfInterest);
+                final double accuracy = CRS.getLinearAccuracy(changeOfCRS);
+                processor.setPositionalAccuracyHints(
+                        (accuracy > 0) ? Quantities.create(accuracy, Units.METRE) : null);
             } catch (FactoryException e) {
                 recoverableException(e);
                 // Leave `changeOfCRS` to null.
@@ -266,7 +251,8 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Applies the image operation (if any) on the given resampled image, than stretches the color ramp.
+     * Applies image operation on the given resampled image.
+     * In current implementation, the only operations are stretching the color ramp.
      *
      * @param  resampledImage  the image computed by {@link #resample(CoordinateReferenceSystem, LinearTransform)}.
      * @param  displayBounds   size and location of the display device, in pixel units.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
index 6816258..42749a8 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -190,7 +190,7 @@ public abstract class MapCanvasAWT extends MapCanvas {
         protected abstract void paint(Graphics2D gr);
 
         /**
-         * Invoked in JavaFX thread after {@link #paint(Graphics2D)} completion. This method can update the
+         * Invoked in JavaFX thread after successful {@link #paint(Graphics2D)} completion. This method can update the
          * {@link #floatingPane} children with the nodes (images, shaped, <i>etc.</i>) created by {@link #render()}.
          * If this method detects that data has changed during the time {@code Renderer} was working in background,
          * then this method can return {@code true} for requesting a new repaint. In such case that repaint will use
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
index ab1ec74..b5fdaee 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/CoordinateOperationFinder.java
@@ -18,6 +18,8 @@ package org.apache.sis.coverage.grid;
 
 import java.util.Arrays;
 import java.util.function.Supplier;
+import javax.measure.Quantity;
+import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.PixelInCell;
@@ -29,6 +31,10 @@ import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.internal.referencing.CoordinateOperations;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 
@@ -199,4 +205,25 @@ final class CoordinateOperationFinder implements Supplier<double[]> {
         }
         return coordinates;
     }
+
+    /**
+     * Configures the accuracy hints on the given processor.
+     */
+    final void setAccuracy(final ImageProcessor processor) {
+        final double accuracy = CRS.getLinearAccuracy(operation);
+        if (accuracy > 0) {
+            Length qm = Quantities.create(accuracy, Units.METRE);
+            Quantity<?>[] hints = processor.getPositionalAccuracyHints();       // Array is already a copy.
+            for (int i=0; i<hints.length; i++) {
+                if (Units.isLinear(hints[i].getUnit())) {
+                    hints[i] = qm;
+                    qm = null;
+                }
+            }
+            if (qm != null) {
+                hints = ArraysExt.append(hints, qm);
+            }
+            processor.setPositionalAccuracyHints(hints);                        // Null elements will be ignored.
+        }
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index fe52dff..5a5deae 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -17,6 +17,7 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Objects;
+import java.security.AccessController;
 import javax.measure.Quantity;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
@@ -26,13 +27,14 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.util.FinalFieldSetter;
 
 
 /**
  * A predefined set of operations on grid coverages as convenience methods.
  *
  * <h2>Thread-safety</h2>
- * {@code GridCoverageProcessor} is thread-safe if its configuration is not modified after construction.
+ * {@code GridCoverageProcessor} is safe for concurrent use in multi-threading environment.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -247,14 +249,19 @@ public class GridCoverageProcessor implements Cloneable {
     /**
      * Returns a coverage processor with the same configuration than this processor.
      *
-     * @return a clone of this image processor.
+     * @return a clone of this coverage processor.
      */
     @Override
     public GridCoverageProcessor clone() {
         try {
-            return (GridCoverageProcessor) super.clone();
+            final GridCoverageProcessor clone = (GridCoverageProcessor) super.clone();
+            AccessController.doPrivileged(new FinalFieldSetter<>(GridCoverageProcessor.class, "imageProcessor"))
+                            .set(clone, imageProcessor.clone());
+            return clone;
         } catch (CloneNotSupportedException e) {
             throw new AssertionError(e);
+        } catch (ReflectiveOperationException e) {
+            throw FinalFieldSetter.cloneFailure(e);
         }
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index e675237..4bc03d9 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -85,11 +85,13 @@ final class ResampledGridCoverage extends GridCoverage {
      * @param  domain          the grid extent, CRS and conversion from cell indices to CRS.
      * @param  toSourceCorner  transform from cell corner coordinates in this coverage to source coverage.
      * @param  toSourceCenter  transform from cell center coordinates in this coverage to source coverage.
+     * @param  changeOfCRS     encapsulate information about the change of CRS.
      * @param  processor       the image processor to use for resampling images.
      */
     private ResampledGridCoverage(final GridCoverage source, final GridGeometry domain,
                                   final MathTransform toSourceCorner,
                                   final MathTransform toSourceCenter,
+                                  final CoordinateOperationFinder changeOfCRS,
                                   ImageProcessor processor)
     {
         super(source, domain);
@@ -112,6 +114,7 @@ final class ResampledGridCoverage extends GridCoverage {
         }
         processor = processor.clone();
         processor.setFillValues(fillValues);
+        changeOfCRS.setAccuracy(processor);
         imageProcessor = GridCoverageProcessor.unique(processor);
     }
 
@@ -392,7 +395,7 @@ final class ResampledGridCoverage extends GridCoverage {
         return new ResampledGridCoverage(source, resampled,
                 MathTransforms.concatenate(targetCornerToCRS, sourceCornerToCRS.inverse()),
                 MathTransforms.concatenate(targetCenterToCRS, sourceCenterToCRS.inverse()),
-                processor).specialize(isGeometryExplicit);
+                changeOfCRS, processor).specialize(isGeometryExplicit);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index edf940f..05fa35e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -30,7 +30,6 @@ import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.ImagingOpException;
 import javax.measure.Quantity;
-import javax.measure.Unit;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.ArraysExt;
@@ -87,7 +86,7 @@ import org.apache.sis.measure.Units;
  * </ul>
  *
  * <h2>Thread-safety</h2>
- * {@code ImageProcessor} is thread-safe if its configuration is not modified after construction.
+ * {@code ImageProcessor} is safe for concurrent use in multi-threading environment.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -125,6 +124,7 @@ public class ImageProcessor implements Cloneable {
     /**
      * The values to use for pixels that can not be computed.
      * This array may be {@code null} or may contain {@code null} elements.
+     * This is a "copy on write" array (elements are not modified).
      *
      * @see #getFillValues()
      * @see #setFillValues(Number...)
@@ -247,7 +247,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @see #resample(RenderedImage, Rectangle, MathTransform)
      */
-    public Interpolation getInterpolation() {
+    public synchronized Interpolation getInterpolation() {
         return interpolation;
     }
 
@@ -258,7 +258,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @see #resample(RenderedImage, Rectangle, MathTransform)
      */
-    public void setInterpolation(final Interpolation method) {
+    public synchronized void setInterpolation(final Interpolation method) {
         ArgumentChecks.ensureNonNull("method", method);
         interpolation = method;
     }
@@ -269,7 +269,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @return fill values to use for pixels that can not be computed, or {@code null} for the defaults.
      */
-    public Number[] getFillValues() {
+    public synchronized Number[] getFillValues() {
         return (fillValues != null) ? fillValues.clone() : null;
     }
 
@@ -282,7 +282,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @param  values  fill values to use for pixels that can not be computed, or {@code null} for the defaults.
      */
-    public void setFillValues(final Number... values) {
+    public synchronized void setFillValues(final Number... values) {
         fillValues = (values != null) ? values.clone() : null;
     }
 
@@ -293,7 +293,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @return whether the operations can be executed in parallel.
      */
-    public Mode getExecutionMode() {
+    public synchronized Mode getExecutionMode() {
         return executionMode;
     }
 
@@ -310,15 +310,17 @@ public class ImageProcessor implements Cloneable {
      *
      * @param  mode  whether the operations can be executed in parallel.
      */
-    public void setExecutionMode(final Mode mode) {
+    public synchronized void setExecutionMode(final Mode mode) {
         ArgumentChecks.ensureNonNull("mode", mode);
         executionMode = mode;
     }
 
     /**
      * Whether the operations can be executed in parallel for the specified image.
+     * This method shall be invoked in a method synchronized on {@code this}.
      */
     private boolean parallel(final RenderedImage source) {
+        assert Thread.holdsLock(this);
         switch (executionMode) {
             case PARALLEL:   return true;
             case SEQUENTIAL: return false;
@@ -334,7 +336,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @return desired accuracy in no particular order, or an empty array if none.
      */
-    public Quantity<?>[] getPositionalAccuracyHints() {
+    public synchronized Quantity<?>[] getPositionalAccuracyHints() {
         return (positionalAccuracyHints != null) ? positionalAccuracyHints.clone() : new Quantity<?>[0];
     }
 
@@ -359,7 +361,7 @@ public class ImageProcessor implements Cloneable {
      * @param  hints  desired accuracy in no particular order, or a {@code null} array if none.
      *                Null elements in the array are ignored.
      */
-    public void setPositionalAccuracyHints(final Quantity<?>... hints) {
+    public synchronized void setPositionalAccuracyHints(final Quantity<?>... hints) {
         if (hints != null) {
             final Quantity<?>[] copy = new Quantity<?>[hints.length];
             int n = 0;
@@ -375,31 +377,13 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
-     * Returns the smallest resolution in the given units.
-     * Current implementation checks only quantities having exactly the specified units
-     * because we look only for {@link Units#PIXEL}. A future version will need to apply
-     * unit conversions if we look for other kind of quantities such as metres.
-     */
-    private double getPositionalAccuracy(final Unit<?> unit) {
-        double accuracy = Double.POSITIVE_INFINITY;
-        if (positionalAccuracyHints != null) {
-            for (final Quantity<?> hint : positionalAccuracyHints) {
-                if (unit.equals(hint.getUnit())) {              // See comment in javadoc.
-                    accuracy = Math.min(accuracy, Math.abs(hint.getValue().doubleValue()));
-                }
-            }
-        }
-        return Double.isFinite(accuracy) ? accuracy : 0;
-    }
-
-    /**
      * Returns whether exceptions occurring during computation are propagated or logged.
      * If {@link ErrorAction#THROW} (the default), exceptions are wrapped in {@link ImagingOpException} and thrown.
      * If any other value, exceptions are wrapped in a {@link LogRecord}, filtered then eventually logged.
      *
      * @return whether exceptions occurring during computation are propagated or logged.
      */
-    public Filter getErrorAction() {
+    public synchronized Filter getErrorAction() {
         return errorAction;
     }
 
@@ -413,15 +397,17 @@ public class ImageProcessor implements Cloneable {
      * @param  action  filter to notify when an operation failed on one or more tiles,
      *                 or {@link ErrorAction#THROW} for propagating the exception.
      */
-    public void setErrorAction(final Filter action) {
+    public synchronized void setErrorAction(final Filter action) {
         ArgumentChecks.ensureNonNull("action", action);
         errorAction = action;
     }
 
     /**
      * Whether errors occurring during computation should be propagated instead than wrapped in a {@link LogRecord}.
+     * This method shall be invoked in a method synchronized on {@code this}.
      */
     private boolean failOnException() {
+        assert Thread.holdsLock(this);
         return errorAction == ErrorAction.THROW;
     }
 
@@ -429,8 +415,10 @@ public class ImageProcessor implements Cloneable {
      * Where to send exceptions (wrapped in {@link LogRecord}) if an operation failed on one or more tiles.
      * Only one log record is created for all tiles that failed for the same operation on the same image.
      * This is always {@code null} if {@link #failOnException()} is {@code true}.
+     * This method shall be invoked in a method synchronized on {@code this}.
      */
     private Filter errorListener() {
+        assert Thread.holdsLock(this);
         return (errorAction instanceof ErrorAction) ? null : errorAction;
     }
 
@@ -453,10 +441,17 @@ public class ImageProcessor implements Cloneable {
         ArgumentChecks.ensureNonNull("source", source);
         Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
         if (!(property instanceof Statistics[])) {
+            final boolean parallel, failOnException;
+            final Filter errorListener;
+            synchronized (this) {
+                parallel        = parallel(source);
+                failOnException = failOnException();
+                errorListener   = errorListener();
+            }
             final StatisticsCalculator calculator = new StatisticsCalculator(
-                    source, areaOfInterest, parallel(source), failOnException());
+                    source, areaOfInterest, parallel, failOnException);
             property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
-            calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener());
+            calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener);
         }
         return (Statistics[]) property;
     }
@@ -476,8 +471,15 @@ public class ImageProcessor implements Cloneable {
      * @see StatisticsCalculator#STATISTICS_KEY
      */
     public RenderedImage statistics(final RenderedImage source, final Shape areaOfInterest) {
-        return (source == null) || ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)
-                ? source : unique(new StatisticsCalculator(source, areaOfInterest, parallel(source), failOnException()));
+        if (source == null || ArraysExt.contains(source.getPropertyNames(), StatisticsCalculator.STATISTICS_KEY)) {
+            return source;
+        }
+        final boolean parallel, failOnException;
+        synchronized (this) {
+            parallel        = parallel(source);
+            failOnException = failOnException();
+        }
+        return unique(new StatisticsCalculator(source, areaOfInterest, parallel, failOnException));
     }
 
     /**
@@ -600,8 +602,20 @@ public class ImageProcessor implements Cloneable {
                     }
                 }
             }
-            resampled = unique(new ResampledImage(source, bounds, toSource, interpolation,
-                                (float) getPositionalAccuracy(Units.PIXEL), fillValues));
+            /*
+             * All accesses to ImageProcessor fields done by this method should be isolated in this single
+             * synchronized block. All arrays are "copy on write", so they do not need to be cloned.
+             */
+            final Interpolation interpolation;
+            final Number[]      fillValues;
+            final Quantity<?>[] positionalAccuracyHints;
+            synchronized (this) {
+                interpolation           = this.interpolation;
+                fillValues              = this.fillValues;
+                positionalAccuracyHints = this.positionalAccuracyHints;
+            }
+            resampled = unique(new ResampledImage(source, bounds, toSource,
+                    interpolation, fillValues, positionalAccuracyHints));
             break;
         }
         if (cm != null && !cm.equals(resampled.getColorModel())) {
@@ -632,7 +646,11 @@ public class ImageProcessor implements Cloneable {
         while (source instanceof PrefetchedImage) {
             source = ((PrefetchedImage) source).source;
         }
-        final PrefetchedImage image = new PrefetchedImage(source, areaOfInterest, parallel(source));
+        final boolean parallel;
+        synchronized (this) {
+            parallel = parallel(source);
+        }
+        final PrefetchedImage image = new PrefetchedImage(source, areaOfInterest, parallel);
         return image.isEmpty() ? source : image;
     }
 
@@ -647,10 +665,25 @@ public class ImageProcessor implements Cloneable {
     public boolean equals(final Object object) {
         if (object != null && object.getClass() == getClass()) {
             final ImageProcessor other = (ImageProcessor) object;
-            return errorAction.equals(other.errorAction)   &&
-                 executionMode.equals(other.executionMode) &&
-                 interpolation.equals(other.interpolation) &&
-                 Arrays.equals(fillValues, other.fillValues);
+            final Mode          executionMode;
+            final Filter        errorAction;
+            final Interpolation interpolation;
+            final Number[]      fillValues;
+            final Quantity<?>[] positionalAccuracyHints;
+            synchronized (this) {
+                executionMode           = this.executionMode;
+                errorAction             = this.errorAction;
+                interpolation           = this.interpolation;
+                fillValues              = this.fillValues;
+                positionalAccuracyHints = this.positionalAccuracyHints;
+            }
+            synchronized (other) {
+                return errorAction.equals(other.errorAction)     &&
+                     executionMode.equals(other.executionMode)   &&
+                     interpolation.equals(other.interpolation)   &&
+                     Arrays.equals(fillValues, other.fillValues) &&
+                     Arrays.equals(positionalAccuracyHints, other.positionalAccuracyHints);
+            }
         }
         return false;
     }
@@ -661,8 +694,10 @@ public class ImageProcessor implements Cloneable {
      * @return a hash code value for this processor.
      */
     @Override
-    public int hashCode() {
-        return Objects.hash(getClass(), errorAction, executionMode, interpolation) + 37*Arrays.hashCode(fillValues);
+    public synchronized int hashCode() {
+        return Objects.hash(getClass(), errorAction, executionMode, interpolation)
+                + 37 * Arrays.hashCode(fillValues)
+                + 39 * Arrays.hashCode(positionalAccuracyHints);
     }
 
     /**
@@ -671,7 +706,7 @@ public class ImageProcessor implements Cloneable {
      * @return a clone of this image processor.
      */
     @Override
-    public ImageProcessor clone() {
+    public synchronized ImageProcessor clone() {
         try {
             return (ImageProcessor) super.clone();
         } catch (CloneNotSupportedException e) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 20ed0f5..126ed47 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -124,6 +124,23 @@ public abstract class PlanarImage implements RenderedImage {
     public static final String GRID_GEOMETRY_KEY = "org.apache.sis.GridGeometry";
 
     /**
+     * Estimation of positional accuracy, typically in metres or pixel units. Pixel positions may have limited accuracy
+     * in they are computed by {@linkplain org.opengis.referencing.operation.Transformation coordinate transformations}.
+     * The position may also be inaccurate because of approximation applied for faster rendering.
+     *
+     * <p>Values should be instances of <code>{@link javax.measure.Quantity[]}</code>. The array length
+     * is typically 1 or 2. If accuracy is limited by a coordinate transformation, then the array should contain an
+     * {@linkplain org.apache.sis.referencing.CRS#getLinearAccuracy accuracy expressed in a linear unit} such as meter.
+     * If accuracy is limited by an {@linkplain ImageProcessor#setPositionalAccuracyHints approximation applied during
+     * resampling operation}, then the array should contain an accuracy expressed in
+     * {@linkplain org.apache.sis.measure.Units#PIXEL pixel units}.</p>
+     *
+     * @see ResampledImage#POSITIONAL_CONSISTENCY_KEY
+     * @see org.opengis.referencing.operation.Transformation#getCoordinateOperationAccuracy()
+     */
+    public static final String POSITIONAL_ACCURACY_KEY = "org.apache.sis.PositionalAccuracy";
+
+    /**
      * Key of a property defining the resolutions of sample values in each band. This property is recommended
      * for images having sample values as floating point numbers. For example if sample values were computed by
      * <var>value</var> = <var>integer</var> × <var>scale factor</var>, then the resolution is the scale factor.
@@ -188,6 +205,9 @@ public abstract class PlanarImage implements RenderedImage {
      *     <td>{@value #GRID_GEOMETRY_KEY}</td>
      *     <td>Conversion from pixel coordinates to "real world" coordinates.</td>
      *   </tr><tr>
+     *     <td>{@value #POSITIONAL_ACCURACY_KEY}</td>
+     *     <td>Estimation of positional accuracy, typically in metres or pixel units.</td>
+     *   </tr><tr>
      *     <td>{@value #SAMPLE_RESOLUTIONS_KEY}</td>
      *     <td>Resolutions of sample values in each band.</td>
      *   </tr><tr>
@@ -210,6 +230,7 @@ public abstract class PlanarImage implements RenderedImage {
      */
     @Override
     public Object getProperty(String key) {
+        ArgumentChecks.ensureNonNull("key", key);
         return Image.UndefinedProperty;
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index b6a60c4..28e91a6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -16,10 +16,8 @@
  */
 package org.apache.sis.image;
 
-import java.util.Set;
 import java.util.Arrays;
 import java.util.Objects;
-import java.util.Collections;
 import java.lang.ref.Reference;
 import java.nio.DoubleBuffer;
 import java.awt.Dimension;
@@ -30,6 +28,9 @@ import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
 import java.awt.image.ImagingOpException;
+import javax.measure.Quantity;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform2D;
 import org.opengis.referencing.operation.TransformException;
@@ -41,11 +42,12 @@ import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
 
 
 /**
@@ -77,14 +79,6 @@ import org.apache.sis.measure.NumberRange;
  */
 public class ResampledImage extends ComputedImage {
     /**
-     * The properties to forwards to source image in calls to {@link #getProperty(String)}.
-     * This list may be augmented in any future Apache SIS version.
-     *
-     * @see #getProperty(String)
-     */
-    private static final Set<String> FILTERED_PROPERTIES = Collections.singleton(SAMPLE_RESOLUTIONS_KEY);
-
-    /**
      * Key of a property providing an estimation of positional error for each pixel.
      * Values shall be instances of {@link RenderedImage} with same size and origin than this image.
      * The image should contain a single band where all sample values are error estimations in pixel units
@@ -93,6 +87,8 @@ public class ResampledImage extends ComputedImage {
      * <p>The default implementation transforms all pixel coordinates {@linkplain #toSource to source},
      * then convert them back to pixel coordinates in this image. The result is compared with expected
      * coordinates and the distance is stored in the image.</p>
+     *
+     * @see #POSITIONAL_ACCURACY_KEY
      */
     public static final String POSITIONAL_CONSISTENCY_KEY = "org.apache.sis.PositionalConsistency";
 
@@ -142,6 +138,14 @@ public class ResampledImage extends ComputedImage {
     private final Object fillValues;
 
     /**
+     * The largest accuracy declared in the {@code accuracy} argument given to constructor,
+     * or {@code null} if none. This is for information purpose only.
+     *
+     * @see #getPositionalAccuracy()
+     */
+    private final Quantity<Length> linearAccuracy;
+
+    /**
      * {@link #POSITIONAL_CONSISTENCY_KEY} value, computed when first requested.
      *
      * @see #getPositionalConsistency()
@@ -162,18 +166,20 @@ public class ResampledImage extends ComputedImage {
      * @param  bounds         domain of pixel coordinates of this resampled image.
      * @param  toSource       conversion of pixel coordinates of this image to pixel coordinates of {@code source} image.
      * @param  interpolation  the object to use for performing interpolations.
-     * @param  accuracy       desired positional accuracy in pixel units, or 0 for the best accuracy available.
-     *                        A value such as 0.125 pixel may enable the use of a slightly faster algorithm
-     *                        at the expense of accuracy. This is only a hint honored on a <em>best-effort</em> basis.
      * @param  fillValues     the values to use for pixels in this image that can not be mapped to pixels in source image.
      *                        May be {@code null} or contain {@code null} elements. If shorter than the number of bands,
      *                        missing values are assumed {@code null}. If longer than the number of bands, extraneous
      *                        values are ignored.
+     * @param  accuracy       values of {@value #POSITIONAL_ACCURACY_KEY} property, or {@code null} if none.
+     *                        This constructor may retain only a subset of specified values or replace some of them.
+     *                        If an accuracy is specified in {@linkplain Units#PIXEL pixel units}, then a value such as
+     *                        0.125 pixel may enable the use of a slightly faster algorithm at the expense of accuracy.
+     *                        This is only a hint honored on a <em>best-effort</em> basis.
      *
      * @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
      */
     protected ResampledImage(final RenderedImage source, final Rectangle bounds, final MathTransform toSource,
-                             final Interpolation interpolation, final float accuracy, final Number[] fillValues)
+            final Interpolation interpolation, final Number[] fillValues, final Quantity<?>[] accuracy)
     {
         super(ImageLayout.DEFAULT.createCompatibleSampleModel(source, bounds), source);
         if (source.getWidth() <= 0 || source.getHeight() <= 0) {
@@ -213,12 +219,29 @@ public class ResampledImage extends ComputedImage {
          * If the desired accuracy is large enough, try using a grid of precomputed values for faster operations.
          * This is optional; it is okay to abandon the grid if we can not compute it.
          */
-        if (accuracy >= ResamplingGrid.TOLERANCE) try {
+        Boolean          canUseGrid     = null;
+        Quantity<Length> linearAccuracy = null;
+        if (accuracy != null) {
+            for (final Quantity<?> hint : accuracy) {
+                if (hint != null) {
+                    final Unit<?> unit = hint.getUnit();
+                    if (Units.PIXEL.equals(unit)) {
+                        final boolean c = Math.abs(hint.getValue().doubleValue()) >= ResamplingGrid.TOLERANCE;
+                        if (canUseGrid == null) canUseGrid = c;
+                        else canUseGrid &= c;
+                    } else if (Units.isLinear(unit)) {
+                        linearAccuracy = Quantities.max(linearAccuracy, hint.asType(Length.class));
+                    }
+                }
+            }
+        }
+        if (canUseGrid != null && canUseGrid) try {
             toSourceSupport = ResamplingGrid.getOrCreate(MathTransforms.bidimensional(toSourceSupport), bounds);
         } catch (TransformException | ImagingOpException e) {
             recoverableException("<init>", e);
         }
         this.toSourceSupport = toSourceSupport;
+        this.linearAccuracy  = linearAccuracy;
         /*
          * Copy the `fillValues` either as an `int[]` or `double[]` array, depending on
          * whether the data type is an integer type or not. Null elements default to zero.
@@ -287,6 +310,33 @@ public class ResampledImage extends ComputedImage {
     }
 
     /**
+     * Returns the number of quantities in the array returned by {@link #getPositionalAccuracy()}.
+     */
+    private int getPositionalAccuracyCount() {
+        int n = 0;
+        if (linearAccuracy != null) n++;
+        if (toSourceSupport instanceof ResamplingGrid) n++;
+        return n;
+    }
+
+    /**
+     * Computes the {@value #POSITIONAL_ACCURACY_KEY} value. This method is invoked by {@link #getProperty(String)}
+     * when the {@link #POSITIONAL_ACCURACY_KEY} property value is requested.
+     */
+    @SuppressWarnings("rawtypes")
+    private Quantity<?>[] getPositionalAccuracy() {
+        final Quantity<?>[] accuracy = new Quantity[getPositionalAccuracyCount()];
+        int n = 0;
+        if (linearAccuracy != null) {
+            accuracy[n++] = linearAccuracy;
+        }
+        if (toSourceSupport instanceof ResamplingGrid) {
+            accuracy[n++] = Quantities.create(ResamplingGrid.TOLERANCE, Units.PIXEL);
+        }
+        return accuracy;
+    }
+
+    /**
      * Computes the {@value #POSITIONAL_CONSISTENCY_KEY} value. This method is invoked by {@link #getProperty(String)}
      * when the {@link #POSITIONAL_CONSISTENCY_KEY} property value is requested. The result is saved by weak reference
      * since recomputing this image is rarely requested, and if needed can be recomputed easily.
@@ -376,14 +426,22 @@ public class ResampledImage extends ComputedImage {
      */
     @Override
     public Object getProperty(final String key) {
-        if (FILTERED_PROPERTIES.contains(key)) {
-            return getSource().getProperty(key);
-        } else if (POSITIONAL_CONSISTENCY_KEY.equals(key)) try {
-            return getPositionalConsistency();
-        } catch (TransformException | IllegalArgumentException e) {
-            throw (ImagingOpException) new ImagingOpException(e.getMessage()).initCause(e);
+        switch (key) {
+            case SAMPLE_RESOLUTIONS_KEY: {
+                return getSource().getProperty(key);
+            }
+            case POSITIONAL_ACCURACY_KEY: {
+                return getPositionalAccuracy();
+            }
+            case POSITIONAL_CONSISTENCY_KEY: try {
+                return getPositionalConsistency();
+            } catch (TransformException | IllegalArgumentException e) {
+                throw (ImagingOpException) new ImagingOpException(e.getMessage()).initCause(e);
+            }
+            default: {
+                return super.getProperty(key);
+            }
         }
-        return super.getProperty(key);
     }
 
     /**
@@ -395,21 +453,26 @@ public class ResampledImage extends ComputedImage {
      */
     @Override
     public String[] getPropertyNames() {
+        final String[] inherited = getSource().getPropertyNames();
+        final String[] names = {
+            SAMPLE_RESOLUTIONS_KEY,
+            POSITIONAL_ACCURACY_KEY,
+            POSITIONAL_CONSISTENCY_KEY
+        };
         int n = 0;
-        String[] names = getSource().getPropertyNames();    // Array should be a copy, so we don't copy again.
-        if (names == null) {
-            names = CharSequences.EMPTY_ARRAY;
-        } else for (final String name : names) {
-            if (FILTERED_PROPERTIES.contains(name)) {
-                names[n++] = name;
+        for (final String name : names) {
+            if (name != POSITIONAL_CONSISTENCY_KEY) {
+                if (name == POSITIONAL_ACCURACY_KEY) {
+                    if (getPositionalAccuracyCount() == 0) {
+                        continue;                   // Exclude PositionalAccuracy change.
+                    }
+                } else if (!ArraysExt.contains(inherited, name)) {
+                    continue;                       // Exclude inherited property not defined by source.
+                }
             }
+            names[n++] = name;
         }
-        if (n < names.length) {
-            names[n++] = POSITIONAL_CONSISTENCY_KEY;
-            return ArraysExt.resize(names, n);
-        } else {
-            return ArraysExt.append(names, POSITIONAL_CONSISTENCY_KEY);
-        }
+        return ArraysExt.resize(names, n);
     }
 
     /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
index 16a1575..ac104ba 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ResampledImageTest.java
@@ -270,7 +270,7 @@ public final strictfp class ResampledImageTest extends TestCase {
         } catch (NoninvertibleTransformException e) {
             throw new AssertionError(e);
         }
-        target = new ResampledImage(source, new Rectangle(9, 9), toSource, interpolation, 0, null);
+        target = new ResampledImage(source, new Rectangle(9, 9), toSource, interpolation, null, null);
 
         assertEquals("numXTiles", 1, target.getNumXTiles());
         assertEquals("numYTiles", 1, target.getNumYTiles());
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
index 0519d4e..2fe287d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Quantities.java
@@ -16,14 +16,17 @@
  */
 package org.apache.sis.measure;
 
+import java.util.Objects;
 import javax.measure.Unit;
 import javax.measure.Quantity;
+import javax.measure.UnconvertibleException;
 import javax.measure.UnitConverter;
 import javax.measure.quantity.Time;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
 import javax.measure.format.ParserException;
 import org.apache.sis.util.Static;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 
@@ -39,7 +42,7 @@ import org.apache.sis.util.resources.Errors;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -146,4 +149,75 @@ public final class Quantities extends Static {
         }
         return (Q) quantity;
     }
+
+    /**
+     * Returns the smallest of two quantities. Values are converted to {@linkplain Unit#getSystemUnit() system unit}
+     * before to be compared. If one of the two quantities is {@code null} or has NaN value, then the other quantity
+     * is returned. If the two quantities have equal converted values, then the first quantity is returned.
+     *
+     * @param  <Q>  type of quantities.
+     * @param  q1   the first quantity (can be {@code null}).
+     * @param  q2   the second quantity (can be {@code null}).
+     * @return the smallest of the two given quantities.
+     *
+     * @since 1.1
+     */
+    public static <Q extends Quantity<Q>> Quantity<Q> min(final Quantity<Q> q1, final Quantity<Q> q2) {
+        return minOrMax(q1, q2, false);
+    }
+
+    /**
+     * Returns the largest of two quantities. Values are converted to {@linkplain Unit#getSystemUnit() system unit}
+     * before to be compared. If one of the two quantities is {@code null} or has NaN value, then the other quantity
+     * is returned. If the two quantities have equal converted values, then the first quantity is returned.
+     *
+     * @param  <Q>  type of quantities.
+     * @param  q1   the first quantity (can be {@code null}).
+     * @param  q2   the second quantity (can be {@code null}).
+     * @return the largest of the two given quantities.
+     *
+     * @since 1.1
+     */
+    public static <Q extends Quantity<Q>> Quantity<Q> max(final Quantity<Q> q1, final Quantity<Q> q2) {
+        return minOrMax(q1, q2, true);
+    }
+
+    /**
+     * Implementation of {@link #min(Quantity, Quantity)} and {@link #max(Quantity, Quantity)}.
+     */
+    private static <Q extends Quantity<Q>> Quantity<Q> minOrMax(final Quantity<Q> q1, final Quantity<Q> q2, final boolean max) {
+        if (q1 == null) return q2;
+        if (q2 == null) return q1;
+        final Unit<Q> u1 = q1.getUnit();
+        final Unit<Q> u2 = q2.getUnit();
+        final Unit<Q> s1 = u1.getSystemUnit();
+        final Unit<Q> s2 = u2.getSystemUnit();
+        if (!Objects.equals(s1, s2)) {
+            throw new UnconvertibleException((String) null);
+        }
+        Number v1 = u1.getConverterTo(s1).convert(q1.getValue());
+        Number v2 = u2.getConverterTo(s2).convert(q2.getValue());
+        if (Numbers.isNaN(v2)) return q1;
+        if (Numbers.isNaN(v1)) return q2;
+        /*
+         * If the two types are instances of `Scalar`, we can compare them directly. Otherwise convert the
+         * `Scalar` type (if any) to `Double` type, then convert again to the widest type of both values.
+         */
+        final boolean t1 = (v1 instanceof Scalar);
+        final boolean t2 = (v2 instanceof Scalar);
+        if (!(t1 & t2)) {
+            if (t1) v1 = v1.doubleValue();
+            if (t2) v2 = v2.doubleValue();
+            final Class<? extends Number> type = Numbers.widestClass(v1, v2);
+            v1 = Numbers.cast(v1, type);
+            v2 = Numbers.cast(v2, type);
+        }
+        /*
+         * Both v1 and v2 are instance of `Comparable<?>` because `Numbers.widestClass(…)`
+         * accepts only known number types such as `Integer`, `Float`, `BigDecimal`, etc.
+         */
+        @SuppressWarnings("unchecked")
+        final int c = ((Comparable) v1).compareTo((Comparable) v2);
+        return (max ? c >= 0 : c <= 0) ? q1 : q2;
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java b/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
index bdc01ec..c922291 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
@@ -199,6 +199,24 @@ public final class Numbers extends Static {
     }
 
     /**
+     * Returns {@code true} if the given number is NaN.
+     * Current implementation recognizes {@link Float} and {@link Double} types.
+     *
+     * @param  value  the number to test (may be {@code null}).
+     * @return {@code true} if the given number if non-null and NaN.
+     *
+     * @see Float#isNaN()
+     * @see Double#isNaN()
+     *
+     * @since 1.1
+     */
+    public static boolean isNaN(final Number value) {
+        if (value instanceof Double) return ((Double) value).isNaN();
+        if (value instanceof Float)  return ((Float)  value).isNaN();
+        return false;
+    }
+
+    /**
      * Returns the number of bits used by primitive of the specified type.
      * The given type must be a primitive type or its wrapper class.
      *
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
index bfab6bf..4a38e79 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/QuantitiesTest.java
@@ -31,7 +31,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link Quantities}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.8
  * @module
  */
@@ -127,4 +127,18 @@ public final strictfp class QuantitiesTest extends TestCase {
         assertTrue (q1.equals(q2));
         assertFalse(q1.equals(q3));
     }
+
+    /**
+     * Tests {@link Quantities#min(Quantity, Quantity)} and {@link Quantities#max(Quantity, Quantity)}.
+     */
+    @Test
+    public void testMinAndMax() {
+        Quantity<Length> q1 = Quantities.create(5,      Units.KILOMETRE);
+        Quantity<Length> q2 = Quantities.create(600,    Units.METRE);
+        Quantity<Length> q3 = Quantities.create(700000, Units.CENTIMETRE);
+        assertSame(q2, Quantities.min(q1, q2));
+        assertSame(q1, Quantities.max(q1, q2));
+        assertSame(q1, Quantities.min(q1, q3));
+        assertSame(q3, Quantities.max(q1, q3));
+    }
 }


Mime
View raw message