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 <[email protected]> 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"; /**
