sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/03: Do not draw isolines that are outside the viewed area.
Date Thu, 14 Jan 2021 16:35:39 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

commit ad06a545df84205954b2a9b6fa4e9ef9bf5c0273
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Jan 14 17:04:15 2021 +0100

    Do not draw isolines that are outside the viewed area.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    | 53 ++++++++++++---------
 .../apache/sis/gui/coverage/IsolineRenderer.java   | 15 ++++--
 .../org/apache/sis/gui/coverage/RenderingData.java | 16 ++-----
 .../apache/sis/internal/feature/j2d/FlatShape.java | 14 +++++-
 .../sis/internal/feature/j2d/MultiPolylines.java   | 25 ++++++++++
 .../org/apache/sis/portrayal/PlanarCanvas.java     | 54 +++++++++++++++++++++-
 .../operation/matrix/AffineTransforms2D.java       |  5 +-
 .../org/apache/sis/internal/system/Modules.java    |  5 ++
 8 files changed, 145 insertions(+), 42 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 6e44e14..60d4f73 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
@@ -30,6 +30,7 @@ import java.awt.Stroke;
 import java.awt.BasicStroke;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Rectangle2D;
 import java.lang.ref.Reference;
 import javafx.scene.paint.Color;
 import javafx.scene.layout.Region;
@@ -548,6 +549,13 @@ public class CoverageCanvas extends MapCanvasAWT {
         private final Envelope2D displayBounds;
 
         /**
+         * Value of {@link CoverageCanvas#getAreaOfInterest()} at the time this worker has
been initialized.
+         * This is the bounds of the are shown in the widget, converted to objective CRS.
+         * This is needed only if {@link #isolines} is non-null.
+         */
+        private Rectangle2D objectiveAOI;
+
+        /**
          * The coordinates of the point to show typically (but not necessarily) in the center
of display area.
          * The coordinate is expressed in objective CRS.
          */
@@ -616,6 +624,7 @@ public class CoverageCanvas extends MapCanvasAWT {
             }
             if (canvas.isolines != null) {
                 isolines = canvas.isolines.prepare();
+                objectiveAOI = canvas.getAreaOfInterest();
             }
         }
 
@@ -707,7 +716,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                 gr.setStroke(new BasicStroke(0));
                 gr.transform((AffineTransform) objectiveToDisplay);     // This cast is safe
in PlanarCanvas subclass.
                 for (final IsolineRenderer.Snapshot s : isolines) {
-                    s.paint(gr);
+                    s.paint(gr, objectiveAOI);
                 }
                 gr.setTransform(at);
                 gr.setStroke(st);
@@ -861,33 +870,31 @@ public class CoverageCanvas extends MapCanvasAWT {
         try {
             table.nextLine('═');
             getGridGeometry().getGeographicExtent().ifPresent((bbox) -> {
-                table.append("Geographic bounding box of display canvas:")
-                     .append(String.format("%nLongitudes: % 10.5f … % 10.5f%n"
-                                           + "Latitudes:  % 10.5f … % 10.5f",
-                             bbox.getWestBoundLongitude(), bbox.getEastBoundLongitude(),
-                             bbox.getSouthBoundLatitude(), bbox.getNorthBoundLatitude()))
+                table.append(String.format("Canvas geographic bounding box (λ,ɸ):%n"
+                             + "Max: % 10.5f°  % 10.5f°%n"
+                             + "Min: % 10.5f°  % 10.5f°",
+                             bbox.getEastBoundLongitude(), bbox.getNorthBoundLatitude(),
+                             bbox.getWestBoundLongitude(), bbox.getSouthBoundLatitude()))
                      .appendHorizontalSeparator();
             });
+            final Rectangle2D aoi = getAreaOfInterest();
             final DirectPosition poi = getPointOfInterest(true);
-            if (poi != null) {
-                table.append("Median in objective CRS:");
-                final int dimension = poi.getDimension();
-                for (int i=0; i<dimension; i++) {
-                    table.append(String.format("%n%, 16.4f", poi.getOrdinate(i)));
-                }
-                table.appendHorizontalSeparator();
+            if (aoi != null && poi != null) {
+                table.append(String.format("A/P of interest in objective CRS (x,y):%n"
+                             + "Max: %, 16.4f  %, 16.4f%n"
+                             + "POI: %, 16.4f  %, 16.4f%n"
+                             + "Min: %, 16.4f  %, 16.4f%n",
+                             aoi.getMaxX(),      aoi.getMaxY(),
+                             poi.getOrdinate(0), poi.getOrdinate(1),
+                             aoi.getMinX(),      aoi.getMinY()))
+                     .appendHorizontalSeparator();
             }
-            final Envelope2D bounds = getDisplayBounds();
-            final Rectangle db = data.displayToData(bounds, getObjectiveToDisplay().inverse());
-            table.append("Display bounds in objective CRS:").append(lineSeparator);
-            final int dimension = bounds.getDimension();
-            for (int i=0; i<dimension; i++) {
-                table.append(String.format("%, 16.4f … %, 16.4f%n", bounds.getMinimum(i),
bounds.getMaximum(i)));
+            final Rectangle source = data.objectiveToData(aoi);
+            if (source != null) {
+                table.append("Extent in source coverage:").append(lineSeparator)
+                     .append(String.valueOf(new GridExtent(source))).append(lineSeparator)
+                     .nextLine();
             }
-            table.appendHorizontalSeparator();
-            table.append("Display bounds in source coverage pixels:").append(lineSeparator)
-                 .append(String.valueOf(new GridExtent(db))).append(lineSeparator)
-                 .nextLine();
             table.nextLine('═');
             table.flush();
         } catch (RenderException | TransformException | IOException e) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
index 943c391..12116c6 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineRenderer.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
 import java.awt.Shape;
 import java.awt.Color;
 import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
 import java.awt.image.RenderedImage;
 import javafx.application.Platform;
 import javafx.scene.control.TableView;
@@ -39,6 +40,7 @@ import org.apache.sis.internal.processing.image.Isolines;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.gui.control.ValueColorMapper.Step;
 import org.apache.sis.internal.feature.j2d.EmptyShape;
+import org.apache.sis.internal.feature.j2d.FlatShape;
 import org.apache.sis.util.ArraysExt;
 
 
@@ -285,7 +287,7 @@ final class IsolineRenderer {
     /**
      * Continues isoline preparation by computing the missing Java2D shapes.
      * This method shall be invoked in a background thread. After this call,
-     * isolines can be painted with {@link Snapshot#paint(Graphics2D)}.
+     * isolines can be painted with {@link Snapshot#paint(Graphics2D, Rectangle2D)}.
      *
      * @param  snapshots  value of {@link #prepare()}. Shall not be {@code null}.
      * @param  data       the source of data. Used only if there is new isolines to compute.
@@ -433,12 +435,17 @@ final class IsolineRenderer {
          * Paints all isolines in the given graphics.
          * This method should be invoked in a background thread.
          *
-         * @param  target  where to draw isolines.
+         * @param  target          where to draw isolines.
+         * @param  areaOfInterest  the area where isolines will be drawn, or {@code null}
if unknown.
          */
-        final void paint(final Graphics2D target) {
+        final void paint(final Graphics2D target, final Rectangle2D areaOfInterest) {
             for (int i=0; i<count; i++) {
-                final Shape shape = shapes[i];
+                Shape shape = shapes[i];
                 if (shape != null) {
+                    if (areaOfInterest != null && shape instanceof FlatShape) {
+                        shape = ((FlatShape) shape).fastClip(areaOfInterest);
+                        if (shape == null) continue;
+                    }
                     target.setColor(new Color(colors[i], true));
                     target.draw(shape);
                 }
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 9ccd224..4ed6077 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
@@ -25,6 +25,7 @@ import java.awt.Graphics2D;
 import java.awt.Rectangle;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
+import java.awt.geom.Rectangle2D;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
@@ -497,21 +498,14 @@ final class RenderingData implements Cloneable {
     }
 
     /**
-     * Converts the given bounds from pixel coordinates on the screen to pixel coordinates
in the source coverage.
-     * As a side effect, the given {@code bounds} rectangle is updated to display bounds
in objective CRS.
+     * Converts the given bounds from objective coordinates to pixel coordinates in the source
coverage.
      *
-     * @param  bounds              display coordinates (in pixels).
-     * @param  displayToObjective  inverse of {@link CoverageCanvas#getObjectiveToDisplay()}.
-     *         Equivalent to {@link #displayToObjective} on the first rendering for a new
zoom level,
-     *         before translations are applied by pan actions.
+     * @param  bounds  objective coordinates.
      * @return data coverage cell coordinates (in pixels).
      * @throws TransformException if the bounds can not be transformed.
      */
-    final Rectangle displayToData(final Envelope2D bounds, final LinearTransform displayToObjective)
throws TransformException {
-        return (Rectangle) Shapes2D.transform(
-                MathTransforms.bidimensional(objectiveToCenter),
-                Shapes2D.transform(MathTransforms.bidimensional(displayToObjective), bounds,
bounds),
-                new Rectangle());
+    final Rectangle objectiveToData(final Rectangle2D bounds) throws TransformException {
+        return (Rectangle) Shapes2D.transform(MathTransforms.bidimensional(objectiveToCenter),
bounds, new Rectangle());
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
index 9c5e738..b472d36 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
@@ -35,7 +35,7 @@ import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
  * @since   1.1
  * @module
  */
-abstract class FlatShape extends AbstractGeometry implements Shape {
+public abstract class FlatShape extends AbstractGeometry implements Shape {
     /**
      * Cached values of shape bounds.
      *
@@ -114,4 +114,16 @@ abstract class FlatShape extends AbstractGeometry implements Shape {
     public final PathIterator getPathIterator(final AffineTransform at, final double flatness)
{
         return getPathIterator(at);
     }
+
+    /**
+     * Returns a potentially smaller shape containing all polylines that intersect the given
area of interest.
+     * This method performs only a quick check based on bounds intersections. It does not
test individual points.
+     * The returned shape may still have many points outside the given bounds.
+     *
+     * @param  areaOfInterest  the area of interest. Edges are considered exclusive.
+     * @return a potentially smaller shape, or {@code null} if this shape is fully outside
the AOI.
+     */
+    public FlatShape fastClip(final Rectangle2D areaOfInterest) {
+        return bounds.intersects(areaOfInterest) ? this : null;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
index 626ac3e..aea1b7a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
@@ -153,4 +153,29 @@ final class MultiPolylines extends FlatShape {
         final Iterator<Polyline> it = Arrays.asList(polylines).iterator();
         return it.hasNext() ? new Polyline.Iter(at, it.next(), it) : new Polyline.Iter();
     }
+
+    /**
+     * Returns a potentially smaller shape containing all polylines that intersect the given
area of interest.
+     * This method performs only a quick check based on bounds intersections.
+     * The returned shape may still have many points outside the given bounds.
+     */
+    @Override
+    public FlatShape fastClip(final Rectangle2D areaOfInterest) {
+        if (bounds.intersects(areaOfInterest)) {
+            final Polyline[] clipped = new Polyline[polylines.length];
+            int count = 0;
+            for (final Polyline p : polylines) {
+                if (p.bounds.intersects(areaOfInterest)) {
+                    clipped[count++] = p;
+                }
+            }
+            if (count != 0) {
+                if (count == polylines.length) {
+                    return this;
+                }
+                return new MultiPolylines(Arrays.copyOf(clipped, count));
+            }
+        }
+        return null;
+    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
index 394ef2b..2edef76 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
@@ -17,10 +17,12 @@
 package org.apache.sis.portrayal;
 
 import java.util.Locale;
+import java.awt.geom.Rectangle2D;
 import java.awt.geom.AffineTransform;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.measure.Units;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.DirectPosition2D;
@@ -28,6 +30,8 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.util.logging.Logging;
 
 
 /**
@@ -64,6 +68,12 @@ public abstract class PlanarCanvas extends Canvas {
     protected final AffineTransform objectiveToDisplay;
 
     /**
+     * The display bounds in objective CRS, or {@code null} if this value needs to be recomputed.
+     * This value is invalidated every time that {@link #objectiveToDisplay} transform changes.
+     */
+    private Rectangle2D areaOfInterest;
+
+    /**
      * Creates a new two-dimensional canvas.
      *
      * @param  locale  the locale to use for labels and some messages, or {@code null} for
default.
@@ -104,6 +114,19 @@ public abstract class PlanarCanvas extends Canvas {
     }
 
     /**
+     * Sets the size and location of the display device in pixel coordinates.
+     * The given envelope shall be two-dimensional. 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.
+     *
+     * @see #getDisplayBounds()
+     */
+    @Override
+    public void setDisplayBounds(final Envelope newValue) throws RenderException {
+        areaOfInterest = null;
+        super.setDisplayBounds(newValue);
+    }
+
+    /**
      * 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.
      *
@@ -112,7 +135,7 @@ public abstract class PlanarCanvas extends Canvas {
      * The returned envelope is a copy; display changes happening after this method invocation
will not be
      * reflected in the returned envelope.</p>
      *
-     * @return size and location of the display device.
+     * @return size and location of the display device in pixel coordinates.
      *
      * @see #setDisplayBounds(Envelope)
      */
@@ -122,6 +145,32 @@ public abstract class PlanarCanvas extends Canvas {
     }
 
     /**
+     * Returns the bounds of the currently visible area in objective CRS.
+     * New Area Of Interests (AOI) are computed when the {@linkplain #getDisplayBounds()
display bounds}
+     * or the {@linkplain #getObjectiveToDisplay() objective to display transform} change.
+     * The AOI can be used as a hint, for example in order to clip data for faster rendering.
+     *
+     * @return bounds of currently visible area in objective CRS, or {@code null} if unavailable.
+     */
+    public Rectangle2D getAreaOfInterest() {
+        if (areaOfInterest == null) {
+            final Envelope2D bounds = getDisplayBounds();
+            if (bounds != null) try {
+                /*
+                 * Following cast is safe because of the way `updateObjectiveToDisplay()`
is implemented.
+                 * The `inverse()` method is invoked on `LinearTransform` instead than `AffineTransform`
+                 * because the former is cached.
+                 */
+                final AffineTransform displayToObjective = (AffineTransform) super.getObjectiveToDisplay().inverse();
+                areaOfInterest = AffineTransforms2D.transform(displayToObjective, bounds,
null);
+            } catch (NoninvertibleTransformException e) {
+                Logging.unexpectedException(Logging.getLogger(Modules.PORTRAYAL), PlanarCanvas.class,
"getAreaOfInterest", e);
+            }
+        }
+        return (areaOfInterest != null) ? (Rectangle2D) areaOfInterest.clone() : null;
+    }
+
+    /**
      * 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
@@ -153,6 +202,7 @@ public abstract class PlanarCanvas extends Canvas {
      */
     @Override
     final void updateObjectiveToDisplay(final LinearTransform newValue) {
+        areaOfInterest = null;
         objectiveToDisplay.setTransform(AffineTransforms2D.castOrCopy(newValue.getMatrix()));
         super.updateObjectiveToDisplay(newValue);
     }
@@ -166,6 +216,7 @@ public abstract class PlanarCanvas extends Canvas {
      */
     public void transformObjectiveCoordinates(final AffineTransform before) {
         if (!before.isIdentity()) {
+            areaOfInterest = null;
             final LinearTransform old = hasListener(OBJECTIVE_TO_DISPLAY_PROPERTY) ? getObjectiveToDisplay()
: null;
             objectiveToDisplay.concatenate(before);
             invalidateObjectiveToDisplay(old);
@@ -181,6 +232,7 @@ public abstract class PlanarCanvas extends Canvas {
      */
     public void transformDisplayCoordinates(final AffineTransform after) {
         if (!after.isIdentity()) {
+            areaOfInterest = null;
             final LinearTransform old = hasListener(OBJECTIVE_TO_DISPLAY_PROPERTY) ? getObjectiveToDisplay()
: null;
             objectiveToDisplay.preConcatenate(after);
             invalidateObjectiveToDisplay(old);
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 b2e8363..b0575cb 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
@@ -28,6 +28,7 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.ArgumentChecks;
@@ -250,7 +251,7 @@ public final class AffineTransforms2D extends Static {
             dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
             return dest;
         }
-        return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
+        return new IntervalRectangle(xmin, ymin, xmax, ymax);
     }
 
     /**
@@ -294,7 +295,7 @@ public final class AffineTransforms2D extends Static {
             dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
             return dest;
         }
-        return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
+        return new IntervalRectangle(xmin, ymin, xmax, ymax);
     }
 
     /**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Modules.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Modules.java
index 3cbcf56..9d0b0d0 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Modules.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Modules.java
@@ -100,6 +100,11 @@ public final class Modules {
     /**
      * The {@value} module name.
      */
+    public static final String PORTRAYAL = "org.apache.sis.portrayal";
+
+    /**
+     * The {@value} module name.
+     */
     public static final String CONSOLE = "org.apache.sis.console";
 
     /**


Mime
View raw message