| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.portrayal; |
| |
| import java.util.ArrayList; |
| import java.util.Locale; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.OptionalDouble; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.metadata.extent.GeographicBoundingBox; |
| import org.opengis.metadata.spatial.DimensionNameType; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.EngineeringCRS; |
| import org.opengis.referencing.crs.GeographicCRS; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.TransformException; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.apache.sis.coverage.grid.PixelInCell; |
| import org.opengis.util.FactoryException; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.Localized; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.geometry.GeneralDirectPosition; |
| import org.apache.sis.geometry.GeneralEnvelope; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory; |
| import org.apache.sis.referencing.operation.matrix.Matrices; |
| import org.apache.sis.referencing.operation.matrix.MatrixSIS; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.referencing.operation.transform.LinearTransform; |
| import org.apache.sis.referencing.operation.transform.TransformSeparator; |
| import org.apache.sis.referencing.privy.ReferencingUtilities; |
| import org.apache.sis.referencing.privy.DirectPositionView; |
| import org.apache.sis.referencing.privy.WraparoundApplicator; |
| import org.apache.sis.util.privy.DoubleDouble; |
| import org.apache.sis.coverage.grid.IncompleteGridGeometryException; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.coverage.grid.GridExtent; |
| |
| // Specific to the main and geoapi-3.1 branches: |
| import org.apache.sis.geometry.MismatchedReferenceSystemException; |
| |
| // Specific to the geoapi-3.1 and geoapi-4.0 branches: |
| import org.opengis.coordinate.MismatchedDimensionException; |
| import org.opengis.coverage.CannotEvaluateException; |
| |
| |
| /** |
| * Common abstraction for implementations that manage the display and user manipulation |
| * of spatial graphic elements. This base class makes no assumption about the geometry |
| * of the display device (e.g. flat video monitor using Cartesian coordinate system, |
| * or planetarium dome using spherical coordinate system). |
| * |
| * <p>This {@code Canvas} base class does not draw anything by itself. |
| * Subclasses are responsible for drawing graphic elements. |
| * The visual contents are usually geographic located symbols, features or images, |
| * but some implementations can also manage non-geographic elements like a map scale.</p> |
| * |
| * <p>A {@code Canvas} manages four fundamental properties:</p> |
| * <ul> |
| * <li>The coordinate reference system to use for displaying data.</li> |
| * <li>The location of data to display in all dimensions, including the dimensions |
| * not shown by the display device (for example time).</li> |
| * <li>The size of the display device, in units of the display coordinate system (typically pixels).</li> |
| * <li>The conversion from the Coordinate Reference System to the display coordinate system.</li> |
| * </ul> |
| * |
| * Those properties are explained in more details below. Other information, for example the |
| * geographic bounding box of the data shown on screen, are inferred from above properties. |
| * |
| * <h2>Coordinate Reference Systems</h2> |
| * There are three {@linkplain CoordinateReferenceSystem Coordinate Reference Systems} |
| * involved in the rendering of geospatial data: |
| * |
| * <ol class="verbose"> |
| * <li>The <dfn>data CRS</dfn> is specific to the data to be displayed. |
| * It may be anything convertible to the <i>objective CRS</i>. |
| * Different graphic elements may use different data CRS, |
| * potentially with a different number of dimensions.</li> |
| * <li>The {@linkplain #getObjectiveCRS objective CRS} is the common CRS in which all data |
| * are converted before to be displayed. If the objective CRS involves a map projection, |
| * it determines the deformation of shapes that user will see on the display device. |
| * The objective CRS should have the same number of dimensions as the display device |
| * (often 2). Its domain of validity should be wide enough for encompassing all data. |
| * The {@link CRS#suggestCommonTarget CRS.suggestCommonTarget(…)} method may be helpful |
| * for choosing an objective CRS from a set of data CRS.</li> |
| * <li>The {@linkplain #getDisplayCRS display CRS} is the coordinate system of the display device. |
| * The {@linkplain #getObjectiveToDisplay() conversion from objective CRS to display CRS} |
| * should be an affine transform with a scale, a translation and optionally a rotation. |
| * This conversion changes every time that the user zooms or scrolls on viewed data.</li> |
| * </ol> |
| * |
| * <h2>Location of data to display</h2> |
| * In addition of above-cited Coordinate Reference Systems, a {@code Canvas} also contains a point of interest. |
| * The point of interest is often, but not necessarily, at the center of display area. |
| * It defines the position where {@linkplain #getSpatialResolution() resolutions} will be computed, |
| * and the position to keep fixed when scales and rotations are applied. |
| * |
| * <p>The point of interest can be expressed in any CRS; |
| * it does not need to be the objective CRS or the CRS of any data. |
| * However, the CRS of that point must have enough dimensions for being convertible to the CRS of all data. |
| * This rule implies that the number of dimensions of the point of interest is equal or greater than |
| * the highest number of dimensions found in data. The purpose is not only to specify which point to show in |
| * (typically) the center of the display area, but also to specify which slice to select in all dimensions |
| * not shown by the display device.</p> |
| * |
| * <h3>Example</h3> |
| * If some data have (<var>x</var>,<var>y</var>,<var>z</var>) dimensions and |
| * other data have (<var>x</var>,<var>y</var>,<var>t</var>) dimensions, then the point of interest shall contain |
| * coordinate values for at least all of the (<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>) dimensions |
| * (i.e. it must be 4-dimensional, even if all data in this example are 3-dimensional). If the display device |
| * is a two-dimensional screen showing map in the (<var>x</var>,<var>y</var>) dimensions (horizontal plane), |
| * then the point of interest defines the <var>z</var> value (elevation or depth) and the <var>t</var> value |
| * (date and time) of the slice to show. |
| * |
| * <h2>Display device size</h2> |
| * The geographic extent of data to be rendered is constrained by the zoom level and the display device size. |
| * The display size is given by {@link #getDisplayBounds()} as an envelope having the number of dimensions of |
| * the display device. The display bounds is usually given in {@linkplain Units#PIXEL pixel units}, but other |
| * units such as {@link Units#POINT} are also authorized. |
| * The zoom level is given indirectly by the {@link #getObjectiveToDisplay()} transform. |
| * The display device may have a wraparound axis, for example in the spherical coordinate system of a planetarium. |
| * |
| * <h2>Multi-threading</h2> |
| * {@code Canvas} is not thread-safe. Synchronization, if desired, must be done by the caller. |
| * Another common strategy is to interact with {@code Canvas} from a single thread, |
| * for example the Swing or JavaFX event queue. |
| * |
| * @author Johann Sorel (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.4 |
| * @since 1.1 |
| */ |
| public class Canvas extends Observable implements Localized { |
| /** |
| * The {@value} property name, used for notifications about changes in objective CRS. |
| * The objective CRS is the Coordinate Reference System in which all data are transformed before displaying. |
| * Its number of dimension is the determined by the display device (two for flat screens). |
| * Associated values are instances of {@link CoordinateReferenceSystem}. |
| * |
| * @see #getObjectiveCRS() |
| * @see #setObjectiveCRS(CoordinateReferenceSystem, DirectPosition) |
| * @see #addPropertyChangeListener(String, PropertyChangeListener) |
| */ |
| public static final String OBJECTIVE_CRS_PROPERTY = "objectiveCRS"; |
| |
| /** |
| * The {@value} property name, used for notifications about changes in <i>objective to display</i> conversion. |
| * This conversion maps coordinates in the {@linkplain #getObjectiveCRS() objective CRS} to coordinates in the |
| * {@linkplain #getDisplayCRS() display CRS}. Associated values are instances of {@link LinearTransform}. |
| * The event class is the {@link TransformChangeEvent} specialization. |
| * |
| * @see #getObjectiveToDisplay() |
| * @see #setObjectiveToDisplay(LinearTransform) |
| * @see #addPropertyChangeListener(String, PropertyChangeListener) |
| */ |
| public static final String OBJECTIVE_TO_DISPLAY_PROPERTY = "objectiveToDisplay"; |
| |
| /** |
| * The {@value} property name, used for notifications about changes in bounds of display device. |
| * It may be for example changes in the size of the window were data are shown. |
| * Associated values are instances of {@link Envelope}. |
| * |
| * @see #getDisplayBounds() |
| * @see #setDisplayBounds(Envelope) |
| * @see #addPropertyChangeListener(String, PropertyChangeListener) |
| */ |
| public static final String DISPLAY_BOUNDS_PROPERTY = "displayBounds"; |
| |
| /** |
| * The {@value} property name, used for notifications about changes in point of interest. |
| * The point of interest defines the location of a representative point, |
| * typically (but not necessarily) in the center of the data bounding box. |
| * It defines also the slice coordinate values in all dimensions beyond the ones shown by the device. |
| * Associated values are instances of {@link DirectPosition}. |
| * |
| * @see #getPointOfInterest(boolean) |
| * @see #setPointOfInterest(DirectPosition) |
| * @see #addPropertyChangeListener(String, PropertyChangeListener) |
| */ |
| public static final String POINT_OF_INTEREST_PROPERTY = "pointOfInterest"; |
| |
| /** |
| * The {@value} property name. |
| * The grid geometry is a synthetic property computed from other properties when requested. |
| * The computed grid geometry may change every time that a {@value #OBJECTIVE_CRS_PROPERTY}, |
| * {@value #OBJECTIVE_TO_DISPLAY_PROPERTY}, {@value #DISPLAY_BOUNDS_PROPERTY} or |
| * {@value #POINT_OF_INTEREST_PROPERTY} property is changed. We do not (at this time) fire |
| * {@value} change events because computing a new grid geometry for every changes of above-cited |
| * properties would be costly. An alternative approach could be to fire {@value} event only when |
| * {@link #setGridGeometry(GridGeometry)} is explicitly invoked, but it could be misleading if |
| * it gives the false impression that the grid geometry did not changed because a listener did |
| * not received an {@value} event. |
| * |
| * @see #getGridGeometry() |
| * @see #setGridGeometry(GridGeometry) |
| */ |
| private static final String GRID_GEOMETRY_PROPERTY = "gridGeometry"; |
| |
| /** |
| * The {@value} property name. The geographic area is a synthetic property computed |
| * from {@value #DISPLAY_BOUNDS_PROPERTY}, {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} |
| * and {@value #OBJECTIVE_CRS_PROPERTY}. There are no events fired for this property. |
| * |
| * @see #getGeographicArea() |
| */ |
| private static final String GEOGRAPHIC_AREA_PROPERTY = "geographicArea"; |
| |
| /** |
| * The {@value} property name. The resolution is a synthetic property computed from |
| * {@value #POINT_OF_INTEREST_PROPERTY}, {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} and |
| * {@value #OBJECTIVE_CRS_PROPERTY}. There are no events fired for this property. |
| * |
| * @see #getSpatialResolution() |
| */ |
| private static final String SPATIAL_RESOLUTION_PROPERTY = "spatialResolution"; |
| |
| /** |
| * The coordinate reference system in which to transform all data before displaying. |
| * If {@code null}, then no transformation is applied and data coordinates are used directly |
| * as display coordinates, regardless the data CRS (even if different data use different CRS). |
| * |
| * @see #OBJECTIVE_CRS_PROPERTY |
| * @see #getObjectiveCRS() |
| * @see #setObjectiveCRS(CoordinateReferenceSystem, DirectPosition) |
| * @see #augmentedObjectiveCRS |
| */ |
| private CoordinateReferenceSystem objectiveCRS; |
| |
| /** |
| * The conversion from {@linkplain #getObjectiveCRS() objective CRS} to the display coordinate system. |
| * Conceptually this conversion should never be null (its initial value is the identity conversion). |
| * However, subclasses may use a more specialized type such as {@link java.awt.geom.AffineTransform} |
| * and set this field to {@code null} for recomputing it from the specialized type when requested. |
| * |
| * @see #OBJECTIVE_TO_DISPLAY_PROPERTY |
| * @see #getObjectiveToDisplay() |
| * @see #setObjectiveToDisplay(LinearTransform) |
| */ |
| private LinearTransform objectiveToDisplay; |
| |
| /** |
| * The size and location of the output device, modified in-place if the size change. |
| * The CRS of this envelope is the display CRS. Coordinate values are initially NaN. |
| * |
| * @see #DISPLAY_BOUNDS_PROPERTY |
| * @see #getDisplayBounds() |
| * @see #getDisplayCRS() |
| * @see #setDisplayBounds(Envelope) |
| */ |
| final GeneralEnvelope displayBounds; |
| |
| /** |
| * A point (in display coordinates) considered representative of the data. |
| * This is the default location where Jacobian matrices are computed when needed. |
| * This is typically (but not necessarily) the center of data bounding box. |
| * May become outside the viewing area after zooms or translations have been applied. |
| * |
| * Also used for selecting a slice in all supplemental dimensions. |
| * If {@code null}, then calculations that depend on a point of interest are skipped. |
| * |
| * @see #POINT_OF_INTEREST_PROPERTY |
| * @see #getPointOfInterest(boolean) |
| * @see #setPointOfInterest(DirectPosition) |
| */ |
| private GeneralDirectPosition pointOfInterest; |
| |
| /** |
| * The point of interest transformed to the objective CRS, or {@code null} if {@link #pointOfInterest} |
| * has not yet been provided. This point shall be updated immediately when {@link #pointOfInterest} is |
| * updated, as a way to verify that the point is valid. |
| * |
| * <p>There is no setter method for this property. It is computed from {@link #pointOfInterest} |
| * and {@link #objectiveCRS} (indirectly, through {@link #multidimToObjective}) and should be |
| * recomputed when any of those properties changed.</p> |
| * |
| * @see #getObjectivePOI() |
| * @see #getGridGeometry() |
| */ |
| private DirectPosition objectivePOI; |
| |
| /** |
| * The transform from the multi-dimensional CRS of {@link #pointOfInterest} to the objective CRS. |
| * This is the transform used for computing {@link #objectivePOI}. This transform may reduce the |
| * number of dimensions. |
| * |
| * <p>There is no setter method for this property. It is computed from {@link #pointOfInterest} |
| * and {@link #objectiveCRS} and should be recomputed when any of those properties changed.</p> |
| * |
| * <p>In current implementation, this transform may depend on the zoom level and viewed geographic |
| * area at the time this transform has been computed. The transform could be slightly different if |
| * it has been computed at the time a different geographic area was viewed. Those variations may exist |
| * because {@link #findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem, boolean)} takes |
| * in account the current viewing conditions. We may need to revisit this behavior in the future if |
| * it appears to be a problem.</p> |
| * |
| * @see #getGridGeometry() |
| */ |
| private MathTransform multidimToObjective; |
| |
| /** |
| * The {@link #objectiveCRS} augmented with additional dimensions found in {@link #pointOfInterest}. |
| * This field is initially {@code null} and computed only if needed. It may be reset to {@code null} |
| * at any time if the properties used for computing this value changed. |
| * |
| * <p>If the point of interest has no supplemental dimension, then this CRS is {@link #objectiveCRS}. |
| * Otherwise {@linkplain #supplementalDimensions supplemental dimensions} are added on a best effort |
| * basis: some supplemental dimensions may be missing if we have not been able to separate components |
| * from the Point Of Interest CRS.</p> |
| * |
| * @see #getGridGeometry() |
| */ |
| private CoordinateReferenceSystem augmentedObjectiveCRS; |
| |
| /** |
| * The dimensions in the Point Of Interest CRS that are not in the {@link #objectiveCRS}. |
| * Those dimensions are encoded as a bitmask: if dimension <var>n</var> is a supplemental |
| * dimension, then the bit {@code 1L << n} is set to 1. This encoding implies that we can |
| * not handle more than {@value Long#SIZE} dimensions at this time. |
| * |
| * <p>The value of this field is invalid if {@link #augmentedObjectiveCRS} is {@code null}. |
| * Those two fields are computed together.</p> |
| * |
| * @see #getGridGeometry() |
| */ |
| private long supplementalDimensions; |
| |
| /** |
| * Type of each grid axis (column, row, vertical, temporal, …) or {@code null} if unspecified. |
| * This is only a help for debugging purpose, by providing more information to the developers. |
| * Those types should not be used for any "real" work. |
| * |
| * @see #getGridGeometry() |
| */ |
| private DimensionNameType[] axisTypes; |
| |
| /** |
| * The grid geometry, computed when first needed and reset to {@code null} when invalidated. |
| * This is invalidated when any {@link Canvas} property is modified. In particular, this is |
| * invalidated every time that the {@link #objectiveToDisplay} transform changes. Note that |
| * "objective to display" changes happen much more often than changes in other properties. |
| * |
| * <p>The {@link #augmentedObjectiveCRS}, {@link #supplementalDimensions}, {@link #multidimToObjective}, |
| * {@link #objectivePOI} and {@link #axisTypes} objects are intermediate calculations with typically a |
| * longer lifetime than {@code gridGeometry}. They are saved for faster recomputation of grid geometry |
| * when only the {@link #objectiveToDisplay} transform has changed.</p> |
| */ |
| private GridGeometry gridGeometry; |
| |
| /** |
| * The context (geographic area and desired resolution) for selecting a coordinate operation. |
| * The information contained in this object can opportunistically be used for providing the |
| * geographic area and spatial resolution of this canvas. |
| * |
| * @see #getGeographicArea() |
| * @see #getSpatialResolution() |
| * @see #findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem, boolean) |
| */ |
| private final CanvasContext operationContext; |
| |
| /** |
| * The factory to use for creating coordinate operations. This factory allow us to specify the area |
| * of interest (the geographic region shown by this {@code Canvas}) and the desired resolution. |
| * |
| * @see #findTransform(CoordinateReferenceSystem, CoordinateReferenceSystem, boolean) |
| */ |
| private final DefaultCoordinateOperationFactory coordinateOperationFactory; |
| |
| /** |
| * The locale for labels or error messages, or {@code null} for the default. |
| */ |
| private final Locale locale; |
| |
| /** |
| * Creates a new canvas for a display device using the given coordinate reference system. |
| * The display CRS of a canvas cannot be changed after construction. |
| * Its coordinate system depends on the display device shape |
| * (for example a two-dimensional Cartesian coordinate system for flat screens, |
| * or a polar or spherical coordinate system for planetarium domes). |
| * The axis units of measurement are typically (but not necessarily) {@link Units#PIXEL} |
| * for Cartesian coordinate systems, with {@link Units#DEGREE} in polar, cylindrical or |
| * spherical coordinate systems. |
| * |
| * @param displayCRS the coordinate system of the display device. |
| * @param locale the locale to use for labels and some messages, or {@code null} for default. |
| */ |
| protected Canvas(final EngineeringCRS displayCRS, final Locale locale) { |
| this.locale = locale; |
| ArgumentChecks.ensureNonNull("displayCRS", displayCRS); |
| displayBounds = new GeneralEnvelope(displayCRS); |
| displayBounds.setToNaN(); |
| coordinateOperationFactory = DefaultCoordinateOperationFactory.provider(); |
| operationContext = new CanvasContext(); |
| } |
| |
| /** |
| * Returns the locale used for texts or for producing some error messages. |
| * May be {@code null} if no locale has been specified, in which case |
| * the {@linkplain Locale#getDefault() system default} should be used. |
| * |
| * @return the locale for messages, or {@code null} if not explicitly defined. |
| */ |
| @Override |
| public Locale getLocale() { |
| return locale; |
| } |
| |
| /** |
| * Returns the number of dimensions of the display device. |
| * Subclasses may override for a little bit more efficiency. |
| */ |
| int getDisplayDimensions() { |
| return ReferencingUtilities.getDimension(getDisplayCRS()); |
| } |
| |
| /** |
| * Gets the name of display axes and stores them in the given array. Those display axis names |
| * are used for debugging purposes only, as an additional information provided to developers. |
| * Those names should not be used for any "real" work. The default implementation does nothing |
| * since this base {@link Canvas} class does not know well the geometry of the display device. |
| * It is okay to leave elements to {@code null}. |
| * |
| * @param axisTypes where to store the name of display axes. The array length will be |
| * at least {@link #getDisplayDimensions()} (it will often be longer). |
| */ |
| void getDisplayAxes(final DimensionNameType[] axisTypes) { |
| } |
| |
| /** |
| * Returns the Coordinate Reference System of the display device. |
| * The axis units of measurement are typically (but not necessarily) {@link Units#PIXEL} |
| * for Cartesian coordinate systems, with {@link Units#DEGREE} in polar, cylindrical or |
| * spherical coordinate systems. The coordinate system may have a wraparound axis for |
| * some "exotic" display devices (e.g. planetarium dome). |
| * |
| * <p>Note that the {@link CRS#findOperation CRS.findOperation(…)} static method can generally |
| * not handle this display CRS. To apply coordinate operations on display coordinates, |
| * {@link #getObjectiveToDisplay()} transform must be inverted and used.</p> |
| * |
| * <h4>Usage note</h4> |
| * Invoking this method is rarely needed. |
| * It is sufficient to said that a display CRS exists at least conceptually, |
| * and that we define a conversion from the objective CRS to that display CRS. |
| * This method may be useful when the subclasses may be something else than {@link PlanarCanvas}, |
| * in which case the caller may want more information about the geometry of the display device. |
| * |
| * @return the Coordinate Reference System of the display device. |
| * |
| * @see #getObjectiveCRS() |
| * @see #getObjectiveToDisplay() |
| */ |
| public final EngineeringCRS getDisplayCRS() { |
| return (EngineeringCRS) displayBounds.getCoordinateReferenceSystem(); |
| } |
| |
| /** |
| * Returns the Coordinate Reference System in which all data are transformed before displaying. |
| * After conversion to this CRS, coordinates should be related to the display device coordinates |
| * with only a final scale, a translation and optionally a rotation remaining to apply. |
| * |
| * <p>This value may be {@code null} on newly created {@code Canvas}, before data are added and canvas |
| * is configured. It should not be {@code null} anymore once a {@code Canvas} is ready for displaying.</p> |
| * |
| * @return the Coordinate Reference System in which to transform all data before displaying. |
| * |
| * @see #OBJECTIVE_CRS_PROPERTY |
| * @see #getDisplayCRS() |
| * @see #getObjectiveToDisplay() |
| */ |
| public CoordinateReferenceSystem getObjectiveCRS() { |
| return objectiveCRS; |
| } |
| |
| /** |
| * Sets the Coordinate Reference System in which all data are transformed before displaying. |
| * The new CRS must be compatible with the previous CRS, i.e. a coordinate operation between |
| * the two CRSs shall exist. If this is not the case (e.g. for rendering completely new data), |
| * use {@link #setGridGeometry(GridGeometry)} instead. |
| * |
| * <p>The given CRS should have a domain of validity wide enough for encompassing all data |
| * (the {@link CRS#suggestCommonTarget CRS.suggestCommonTarget(…)} method may be helpful |
| * for choosing an objective CRS from a set of data CRS). |
| * If the given value is different than the previous value, then a change event is sent to |
| * all listeners registered for the {@value #OBJECTIVE_CRS_PROPERTY} property.</p> |
| * |
| * <p>If the transform between old and new CRS is not identity, then this method recomputes the |
| * <i>objective to display</i> conversion in a way preserving the display coordinates of the given anchor, |
| * together with the scales and orientations of features in close neighborhood of that point. |
| * This calculation may cause {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} property change event |
| * with the {@link TransformChangeEvent.Reason#CRS_CHANGE} reason to be sent to listeners. |
| * That event is sent after the above-cited {@value #OBJECTIVE_CRS_PROPERTY} event |
| * (note that {@value #POINT_OF_INTEREST_PROPERTY} stay unchanged). |
| * All those change events are sent only after all property values have been updated to their new values.</p> |
| * |
| * @param newValue the new Coordinate Reference System in which to transform all data before displaying. |
| * @param anchor the point to keep at fixed display coordinates, expressed in any compatible CRS. |
| * If {@code null}, defaults to {@linkplain #getPointOfInterest(boolean) point of interest}. |
| * If non-null, the anchor must be associated to a CRS. |
| * @throws NullPointerException if the given CRS is null. |
| * @throws MismatchedDimensionException if the given CRS does not have the number of dimensions of the display device. |
| * @throws RenderException if the objective CRS cannot be set to the given value for another reason. |
| */ |
| public void setObjectiveCRS(final CoordinateReferenceSystem newValue, DirectPosition anchor) throws RenderException { |
| ArgumentChecks.ensureNonNull(OBJECTIVE_CRS_PROPERTY, newValue); |
| ArgumentChecks.ensureDimensionMatches(OBJECTIVE_CRS_PROPERTY, getDisplayDimensions(), newValue); |
| final CoordinateReferenceSystem oldValue = objectiveCRS; |
| if (!newValue.equals(oldValue)) try { |
| final CoordinateOperation newToGeo = objectiveToGeographic(newValue); |
| LinearTransform oldObjectiveToDisplay = null; |
| LinearTransform newObjectiveToDisplay = null; |
| if (oldValue != null) { |
| /* |
| * Compute the change unconditionally as a way to verify that the new CRS is compatible with |
| * data currently shown. Another reason is that checking identity transform is more reliable |
| * than the `compareIgnoreMetadata(oldValue, newValue)` check. |
| * |
| * Note: we are invoking `findTransform(…)` with a CoordinateOperationContext computed from |
| * the old CRS. But it is okay because the context information are geographic area (degrees) |
| * and approximate resolution (metres), which should not change a lot since we will continue |
| * to view the same area after the CRS change. Those information only need to be approximate |
| * anyway, and in many cases will be totally ignored by `findTransform(…)`. |
| */ |
| final MathTransform newToOld = findTransform(newValue, oldValue, false); |
| if (pointOfInterest != null && !newToOld.isIdentity()) { |
| final CoordinateReferenceSystem poiCRS = pointOfInterest.getCoordinateReferenceSystem(); |
| final MathTransform poiToNew = findTransform(poiCRS, newValue, false); |
| final DirectPosition poiInNew = poiToNew.transform(pointOfInterest, allocatePosition()); |
| /* |
| * We need anchor in new CRS. If no anchor was specified, `poiInNew` is already what we need. |
| * Otherwise convert the anchor to coordinates in the new CRS. There is good chances that the |
| * anchor CRS is the objective CRS, so we can reuse `poiToNew`. |
| */ |
| if (anchor == null) { |
| anchor = poiInNew; |
| } else { |
| final CoordinateReferenceSystem crs = anchor.getCoordinateReferenceSystem(); |
| ArgumentChecks.ensureNonNull("anchor.CRS", crs); |
| if (!Utilities.equalsIgnoreMetadata(crs, newValue)) { |
| MathTransform anchorToNew = poiToNew; |
| if (!Utilities.equalsIgnoreMetadata(crs, poiCRS)) { |
| anchorToNew = findTransform(crs, newValue, true); |
| } |
| anchor = anchorToNew.transform(anchor, allocatePosition()); |
| } |
| } |
| /* |
| * We want pixel coordinates of the Point Of Interest (POI) to be unaffected by the change of CRS, |
| * and the Jacobian matrix around POI to be approximately the same. Conceptually, this is as if we |
| * wanted to convert from new CRS to old CRS before to apply the old `objectiveToCRS` transform. |
| * We get this effect by pre-concatenating a linear approximation of "new to old CRS" transform |
| * before `objectiveToCRS`. That approximation contains only uniform scale, rotation or axis flips |
| * in order to preserve pixel ratios (otherwise the map projection would appear deformed). |
| */ |
| oldObjectiveToDisplay = getObjectiveToDisplay(); |
| final WraparoundApplicator wp = new WraparoundApplicator(null, objectivePOI, oldValue.getCoordinateSystem()); |
| final MathTransform change = orthogonalTangent(wp.forDomainOfUse(newToOld), anchor.getCoordinates()); |
| final MathTransform result = MathTransforms.concatenate(change, oldObjectiveToDisplay); |
| /* |
| * The result is the new `objectiveToTransform` such as the display is unchanged around POI. |
| * That transform should be an instance of `LinearTransform` because the two concatenated |
| * transforms were linear, but we nevertheless invoke `tangent(…)` as a safety; |
| * normally it should just return the `result` as-is. |
| */ |
| newObjectiveToDisplay = MathTransforms.tangent(result, poiInNew); |
| setObjectiveToDisplayImpl(newObjectiveToDisplay); |
| objectivePOI = poiInNew; // Set only after everything else succeeded. |
| multidimToObjective = poiToNew; |
| augmentedObjectiveCRS = null; // Will be recomputed when first needed. |
| axisTypes = null; |
| gridGeometry = null; |
| } |
| } |
| objectiveCRS = newValue; // Set only after everything else succeeded. |
| operationContext.setObjectiveToGeographic(newToGeo); |
| firePropertyChange(OBJECTIVE_CRS_PROPERTY, oldValue, newValue); |
| fireIfChanged(oldObjectiveToDisplay, newObjectiveToDisplay, false); // Shall be after CRS change event. |
| } catch (FactoryException | TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1, OBJECTIVE_CRS_PROPERTY), e); |
| } |
| } |
| |
| /** |
| * Computes the approximate change from a new {@link #objectiveToDisplay} to the old one for keeping the |
| * Point Of Interest (POI) at the same location. The given {@code newToOld} argument is the change as a |
| * potentially non-linear transform. The transform returned by this method is a linear approximation of |
| * {@code newToOld} {@linkplain MathTransforms#tangent tangent} at the POI, but with orthogonal vectors. |
| * In other words, the returned transform may apply a uniform scale, a rotation or flip axes, but no shear. |
| * |
| * @param newToOld the change as a potentially non-linear transform. |
| * @param poiInNew point of interest in the coordinates of the new objective CRS. |
| * @return an approximation of {@code newToOld} with only uniform scale, rotation and axis flips. |
| * |
| * @see MathTransforms#tangent(MathTransform, DirectPosition) |
| */ |
| private static MathTransform orthogonalTangent(final MathTransform newToOld, final double[] poiInNew) |
| throws TransformException, RenderException |
| { |
| final double[] poiInOld = new double[newToOld.getTargetDimensions()]; |
| final MatrixSIS derivative = MatrixSIS.castOrCopy(MathTransforms.derivativeAndTransform(newToOld, poiInNew, 0, poiInOld, 0)); |
| final MatrixSIS magnitudes = derivative.normalizeColumns(); |
| final MatrixSIS affine = Matrices.createAffine(derivative, new DirectPositionView.Double(poiInOld)); |
| final int srcDim = magnitudes.getNumCol(); |
| DoubleDouble scale = DoubleDouble.ZERO; // Will be set to average magnitude value. |
| for (int i=0; i<srcDim; i++) { |
| scale = scale.add(magnitudes.getNumber(0, i), false); |
| } |
| scale = scale.divide(srcDim); |
| /* |
| * Following code assumes a two-dimensional rotation matrix. We have not yet explored how |
| * to generalize to n-dimensional case (Gram–Schmidt process may be a path to explore). |
| * We want: |
| * ┌ ┐ ┌ ┐ |
| * │ m₀₀ m₀₁ │ ≈ │ cos(θ) −sin(θ) │ |
| * │ m₁₀ m₁₁ │ │ sin(θ) cos(θ) │ |
| * └ ┘ └ ┘ |
| * |
| * We want some "average" value for |cos(θ)| (the sign will be adjusted later). |
| * The root mean square (RMS) is convenient because of cos²(θ) = 1 − sin²(θ): |
| * |
| * |cos(θ)| ≈ √((m₀₀² + (1 − m₀₁²) + (1 − m₁₀²) + m₁₁²) / 4) |
| */ |
| if (srcDim == PlanarCanvas.BIDIMENSIONAL && poiInOld.length == PlanarCanvas.BIDIMENSIONAL) { |
| final double ms = Math.max(0, Math.min(1, (cps(affine, 0) + cps(affine, 1) + 2) / 4)); |
| final double sin = Math.sqrt(1 - ms); |
| final double cos = Math.sqrt( ms); |
| for (int row = 0; row <= 1; row++) { |
| final int sor = row ^ 1; |
| affine.setElement(row, row, Math.copySign(cos, affine.getElement(row, row))); |
| affine.setElement(row, sor, Math.copySign(sin, affine.getElement(row, sor))); |
| } |
| } else { |
| throw new RenderException(Errors.format(Errors.Keys.UnsupportedCoordinateSystem_1, "3D")); |
| } |
| for (int i=0; i<srcDim; i++) { |
| affine.convertBefore(i, scale, null); // Use same scale factor for all coordinates. |
| affine.convertBefore(i, null, -poiInNew[i]); |
| } |
| return MathTransforms.linear(affine); |
| } |
| |
| /** |
| * Computes cos(θ)² − sin²(θ) on the given matrix row. Caller needs to add 1 for getting the sum |
| * of squares of cosine values. That addition should be done last for reducing rounding errors. |
| */ |
| private static double cps(final MatrixSIS affine, final int row) { |
| final double cos = affine.getElement(row, row); |
| final double sin = affine.getElement(row, row ^ 1); |
| return cos*cos - sin*sin; |
| } |
| |
| /** |
| * Returns the (usually affine) conversion from objective CRS to display coordinate system. |
| * The source coordinates shall be in the CRS given by {@link #getObjectiveCRS()} and the |
| * converted coordinates will be in the CRS given by {@link #getDisplayCRS()}. |
| * |
| * <p>The <i>objective to display</i> conversion changes every time that user zooms |
| * or scrolls on viewed data. However, the transform returned by this method is a snapshot |
| * taken at the time this method is invoked; subsequent changes in the <i>objective to |
| * display</i> conversion are not reflected in the returned transform.</p> |
| * |
| * @return snapshot of the (usually affine) conversion from objective CRS |
| * to display coordinate system (never {@code null}). |
| * |
| * @see #OBJECTIVE_CRS_PROPERTY |
| * @see #getObjectiveCRS() |
| * @see #getDisplayCRS() |
| */ |
| public LinearTransform getObjectiveToDisplay() { |
| if (objectiveToDisplay == null) { |
| objectiveToDisplay = createObjectiveToDisplay(); |
| } |
| return objectiveToDisplay; |
| } |
| |
| /** |
| * Returns the current <i>objective to display</i> conversion managed by the subclass. |
| * This method is invoked only if {@link #objectiveToDisplay} is {@code null}, which may |
| * happen either at initialization time or if the subclass uses its own specialized field |
| * instead of {@link #objectiveToDisplay} for managing changes in the zooms or viewed area. |
| * This method needs to be overridden only by subclasses using such specialization. |
| * |
| * @return objective to display conversion created from current value managed by subclass. |
| * |
| * @see #setObjectiveToDisplayImpl(LinearTransform) |
| */ |
| LinearTransform createObjectiveToDisplay() { |
| return MathTransforms.identity(getDisplayDimensions()); |
| } |
| |
| /** |
| * Sets the conversion from objective CRS to display coordinate system. |
| * If the given value is different than the previous value, then a change event is sent |
| * to all listeners registered for the {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} property. |
| * The event reason is {@link TransformChangeEvent.Reason#ASSIGNMENT}. |
| * |
| * <p>Invoking this method has the effect of changing the viewed area, the zoom level or the rotation of the map. |
| * It does not update the {@value #POINT_OF_INTEREST_PROPERTY} property however. The point of interest may move |
| * outside the view area as a result of this method call.</p> |
| * |
| * @param newValue the new <i>objective to display</i> conversion. |
| * @throws IllegalArgumentException if given the transform does not have the expected number of dimensions or is not affine. |
| * @throws RenderException if the <i>objective to display</i> transform cannot be set to the given value for another reason. |
| */ |
| public void setObjectiveToDisplay(final LinearTransform newValue) throws RenderException { |
| ArgumentChecks.ensureNonNull(OBJECTIVE_TO_DISPLAY_PROPERTY, newValue); |
| final int expected = getDisplayDimensions(); |
| int actual = newValue.getSourceDimensions(); |
| if (actual == expected) { |
| actual = newValue.getTargetDimensions(); |
| if (actual == expected) { |
| LinearTransform oldValue = objectiveToDisplay; // Do not invoke user-overridable method. |
| if (oldValue == null) { |
| oldValue = createObjectiveToDisplay(); |
| } |
| if (!oldValue.equals(newValue)) { |
| setObjectiveToDisplayImpl(newValue); |
| firePropertyChange(new TransformChangeEvent(this, oldValue, newValue, |
| TransformChangeEvent.Reason.ASSIGNMENT)); |
| } |
| return; |
| } |
| } |
| throw new org.opengis.geometry.MismatchedDimensionException(errors().getString( |
| Errors.Keys.MismatchedDimension_3, OBJECTIVE_TO_DISPLAY_PROPERTY, expected, actual)); |
| } |
| |
| /** |
| * Actually sets the conversion from objective CRS to display coordinate system. |
| * Contrarily to other setter methods, this method does not notify listeners about that change; |
| * it is caller responsibility to fire a {@link TransformChangeEvent} after all fields are updated. |
| * This design choice is because this method is usually invoked as part of a larger set of changes. |
| * |
| * <p>If the new value is {@code null}, then this method only declares that the {@link #objectiveToDisplay} |
| * transform became invalid and will need to be recomputed. It is subclasses responsibility to recompute the |
| * transform in their {@link #createObjectiveToDisplay()}.</p> |
| * |
| * @param newValue the new "objective to display" transform, or {@code null} if it will be computed later |
| * by {@link #createObjectiveToDisplay()}. A null value is okay only when invoked by subclasses that |
| * overrode {@link #createObjectiveToDisplay()}. |
| * |
| * @see #createObjectiveToDisplay() |
| */ |
| void setObjectiveToDisplayImpl(final LinearTransform newValue) { |
| objectiveToDisplay = newValue; |
| gridGeometry = null; |
| operationContext.clear(); |
| } |
| |
| /** |
| * Returns the size and location of the display device. |
| * The unit of measurement is typically (but not necessarily) pixels. |
| * The coordinate values are often integers, but this is not mandatory. |
| * The coordinate reference system is given by {@link #getDisplayCRS()}. |
| * |
| * <p>This value may be {@code null} on newly created {@code Canvas}, before data are added and canvas |
| * is configured. It should not be {@code null} anymore once a {@code Canvas} is ready for displaying.</p> |
| * |
| * @return size and location of the display device. |
| * |
| * @see #DISPLAY_BOUNDS_PROPERTY |
| * @see #getGeographicArea() |
| */ |
| public Envelope getDisplayBounds() { |
| return displayBounds.isAllNaN() ? null : new GeneralEnvelope(displayBounds); |
| } |
| |
| /** |
| * Sets the size and location of the display device. The envelope CRS shall be either the |
| * {@linkplain #getDisplayCRS() display CRS} or unspecified, in which case the display CRS |
| * is assumed. Unit of measurement is typically (but not necessarily) {@link Units#PIXEL}. |
| * If the given value is different than the previous value, then a change event is sent to |
| * all listeners registered for the {@value #DISPLAY_BOUNDS_PROPERTY} property. |
| * |
| * @param newValue the new display bounds. |
| * @throws IllegalArgumentException if the given envelope does not have the expected CRS or number of dimensions. |
| * @throws RenderException if the display bounds cannot be set to the given value for another reason. |
| */ |
| public void setDisplayBounds(final Envelope newValue) throws RenderException { |
| ArgumentChecks.ensureNonNull(DISPLAY_BOUNDS_PROPERTY, newValue); |
| final CoordinateReferenceSystem crs = newValue.getCoordinateReferenceSystem(); |
| if (crs != null && !Utilities.equalsIgnoreMetadata(getDisplayCRS(), crs)) { |
| throw new MismatchedReferenceSystemException(errors().getString( |
| Errors.Keys.IllegalCoordinateSystem_1, IdentifiedObjects.getDisplayName(crs, getLocale()))); |
| } |
| final GeneralEnvelope oldValue = new GeneralEnvelope(displayBounds); |
| displayBounds.setEnvelope(newValue); |
| displayBounds.setCoordinateReferenceSystem(oldValue.getCoordinateReferenceSystem()); |
| if (displayBounds.isEmpty()) { |
| displayBounds.setEnvelope(oldValue); |
| throw new IllegalArgumentException(errors().getString(Errors.Keys.EmptyProperty_1, DISPLAY_BOUNDS_PROPERTY)); |
| } |
| if (!oldValue.equals(displayBounds)) { |
| gridGeometry = null; |
| operationContext.partialClear(false); // Resolution is still valid. |
| firePropertyChange(DISPLAY_BOUNDS_PROPERTY, oldValue, newValue); // Do not publish reference to `displayBounds`. |
| } |
| } |
| |
| /** |
| * Returns the coordinates of a point considered representative of the data. |
| * This is typically (but not necessarily) the center of data bounding box. |
| * This point is used for example as the default location where to compute resolution |
| * (the resolution may vary at each pixel because of map projection deformations). |
| * This position may become outside the viewing area after zooms or translations have been applied. |
| * |
| * <p>The coordinates can be given in their original CRS or in the {@linkplain #getObjectiveCRS() objective CRS}. |
| * If {@code objective} is {@code false}, then the returned position can be expressed in any CRS convertible to |
| * data or objective CRS. If that CRS has more dimensions than the {@linkplain #getObjectiveCRS() objective CRS}, |
| * then the supplemental dimensions specify which slice to show |
| * (for example the depth of the horizontal plane to display, or the date of the dynamic phenomenon to display. |
| * See {@linkplain Canvas class javadoc} for more discussion.) |
| * If {@code objective} is {@code true}, then the position is transformed to the objective CRS.</p> |
| * |
| * <p>This value is initially {@code null}. A value should be specified either by invoking |
| * {@link #setPointOfInterest(DirectPosition)} or {@link #setGridGeometry(GridGeometry)}.</p> |
| * |
| * @param objective whether to return a position transformed to {@linkplain #getObjectiveCRS() objective CRS}. |
| * @return coordinates of a representative point, or {@code null} if unspecified. |
| * |
| * @see #POINT_OF_INTEREST_PROPERTY |
| */ |
| public DirectPosition getPointOfInterest(final boolean objective) { |
| final DirectPosition poi = objective ? objectivePOI : pointOfInterest; |
| return (poi != null) ? new GeneralDirectPosition(poi) : null; |
| } |
| |
| /** |
| * Sets the coordinates of a representative point inside the data bounding box. |
| * If the given value is different than the previous value, then a change event is sent to all listeners |
| * registered for the {@value #POINT_OF_INTEREST_PROPERTY} property. |
| * |
| * @param newValue the new coordinates of a representative point. |
| * @throws NullPointerException if the given position is null. |
| * @throws IllegalArgumentException if the given position does not have a CRS. |
| * @throws RenderException if the point of interest cannot be set to the given value. |
| */ |
| public void setPointOfInterest(final DirectPosition newValue) throws RenderException { |
| ArgumentChecks.ensureNonNull(POINT_OF_INTEREST_PROPERTY, newValue); |
| final GeneralDirectPosition copy = new GeneralDirectPosition(newValue); |
| final CoordinateReferenceSystem crs = copy.getCoordinateReferenceSystem(); |
| if (crs == null) { |
| throw new IllegalArgumentException(errors().getString(Errors.Keys.UnspecifiedCRS)); |
| } |
| final GeneralDirectPosition oldValue = pointOfInterest; |
| if (!copy.equals(oldValue)) try { |
| /* |
| * If the user has not yet specified an objective CRS, takes the Point Of Interest CRS |
| * (only the number of dimensions that the display device can show). |
| */ |
| if (objectiveCRS == null) { |
| final CoordinateReferenceSystem newObjectiveCRS = CRS.getComponentAt(crs, 0, getDisplayDimensions()); |
| if (newObjectiveCRS == null) { |
| throw new IllegalArgumentException("Cannot infer objective CRS."); |
| // Message not localized yet because we should probably try harder. |
| } |
| operationContext.setObjectiveToGeographic(objectiveToGeographic(newObjectiveCRS)); |
| objectiveCRS = newObjectiveCRS; // Set only on success. |
| } |
| /* |
| * Transform the Point Of Interest to the objective CRS as a way to test its validity. |
| * All canvas fields will be updated only if this operation succeeds. |
| * |
| * Note 1: in the CoordinateOperationContext used for selecting a MathTransform, the geographic area is |
| * still the same but the spatial resolution could be slightly different because computed at a new point |
| * of interest. But we cannot use the new point of interest now, because we need the MathTransform for |
| * computing it. However, in practice the resolution is often ignored, or does not vary a lot in regions |
| * where it matter. So we assume it is okay to keep the CoordinateOperationContext with old resolution |
| * in the following call to `findTransform(…)` or usage of `multidimToObjective`. |
| * |
| * Note 2: `oldValue` cannot be null if `multidimToObjective` is non-null. |
| */ |
| MathTransform mt = multidimToObjective; |
| if (mt == null || !Utilities.equalsIgnoreMetadata(crs, oldValue.getCoordinateReferenceSystem())) { |
| mt = findTransform(crs, objectiveCRS, false); |
| } |
| objectivePOI = mt.transform(copy, allocatePosition()); |
| pointOfInterest = copy; // Set only after transform succeeded. |
| multidimToObjective = mt; |
| augmentedObjectiveCRS = null; // Will be recomputed when first needed. |
| axisTypes = null; |
| gridGeometry = null; |
| operationContext.partialClear(true); // Geographic area is still valid. |
| firePropertyChange(POINT_OF_INTEREST_PROPERTY, oldValue, newValue); // Do not publish reference to `copy`. |
| } catch (FactoryException | TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1, POINT_OF_INTEREST_PROPERTY), e); |
| } |
| } |
| |
| /** |
| * Returns the coordinate values of the Point Of Interest (POI) in objective CRS. |
| * The array length should be equal to {@link #getDisplayDimensions()}. |
| * May be {@code null} if the point of interest is unknown. |
| */ |
| final double[] getObjectivePOI() { |
| return (objectivePOI != null) ? objectivePOI.getCoordinates() : null; |
| } |
| |
| /** |
| * Returns canvas properties (CRS, display bounds, conversion) encapsulated in a grid geometry. |
| * This is a convenience method for interoperability with grid coverage API. |
| * If {@link #setGridGeometry(GridGeometry)} has been invoked with a non-null value and no other |
| * {@code Canvas} property changed since that method call, then this method returns that value. |
| * Otherwise this method computes a grid geometry as described below. |
| * |
| * <p>The set of {@link GridGeometry} dimensions includes all the dimensions of the objective CRS, |
| * augmented with all (if possible) or some supplemental dimensions found in the point of interest. |
| * For example if the canvas manages only (<var>x</var>,<var>y</var>) coordinates but the point of |
| * interest includes also a <var>t</var> coordinate, then a third dimension (which we call the |
| * <i>supplemental dimension</i>) for <var>t</var> is added to the CRS, {@link GridExtent} |
| * and "grid to CRS" transform of the returned grid geometry.</p> |
| * |
| * <table class="sis"> |
| * <caption>Canvas properties → grid geometry properties</caption> |
| * <tr> |
| * <th>Grid geometry element</th> |
| * <th>Display dimensions</th> |
| * <th>Supplemental dimensions</th> |
| * </tr><tr> |
| * <td>{@link GridGeometry#getCoordinateReferenceSystem()}</td> |
| * <td>{@link #getObjectiveCRS()}.</td> |
| * <td>Some of <code>{@linkplain #getPointOfInterest(boolean) |
| * getPointOfInterest}(false).getCoordinateReferenceSystem()</code></td> |
| * </tr><tr> |
| * <td>{@link GridGeometry#getExtent()}</td> |
| * <td>{@link #getDisplayBounds()} rounded to enclosing (floor and ceil) integers</td> |
| * <td>[0 … 0]</td> |
| * </tr><tr> |
| * <td>{@link GridGeometry#getGridToCRS(PixelInCell)}</td> |
| * <td>Inverse of {@link #getObjectiveToDisplay()}</td> |
| * <td>Some {@linkplain #getPointOfInterest(boolean) point of interest} coordinates as translation terms</td> |
| * </tr> |
| * </table> |
| * |
| * The {@link GridGeometry#getGridToCRS(PixelInCell)} transform built by this method is always a {@link LinearTransform}. |
| * This linearity implies that the grid geometry CRS cannot be the Point Of Interest (POI) CRS, unless conversion |
| * from POI CRS to objective CRS is linear. |
| * |
| * @return a grid geometry encapsulating canvas properties, including supplemental dimensions if possible. |
| * @throws RenderException if the grid geometry cannot be computed. |
| */ |
| public GridGeometry getGridGeometry() throws RenderException { |
| if (gridGeometry == null) try { |
| /* |
| * If not already done, create a multi-dimensional CRS composed of `objectiveCRS` |
| * with supplemental dimensions appended. This CRS needs to be recreated only if |
| * the Point of Interest and/or the objective CRS changed since last call. |
| */ |
| if (augmentedObjectiveCRS == null) { |
| if (pointOfInterest != null && objectiveCRS != null) { |
| final CoordinateReferenceSystem crs = pointOfInterest.getCoordinateReferenceSystem(); |
| final ArrayList<CoordinateReferenceSystem> components = new ArrayList<>(4); |
| components.add(objectiveCRS); |
| /* |
| * `findSupplementalDimensions(…)` tries to complete the `components` list on a best effort basis. |
| * We have no guarantees that all supplemental dimensions will be included. The set of dimensions |
| * actually appended is encoded in `supplementalDimensions` bits. |
| */ |
| supplementalDimensions = CanvasExtent.findSupplementalDimensions(crs, |
| multidimToObjective.derivative(pointOfInterest), components); |
| augmentedObjectiveCRS = CRS.compound(components.toArray(CoordinateReferenceSystem[]::new)); |
| if (Utilities.equalsIgnoreMetadata(augmentedObjectiveCRS, crs)) { |
| augmentedObjectiveCRS = crs; |
| } |
| } else { |
| augmentedObjectiveCRS = objectiveCRS; |
| } |
| /* |
| * The axis types are for information purposes only, for making debugging easier. |
| * It will typically contains the (column, row) names, maybe completed with up or |
| * time names. |
| */ |
| axisTypes = CanvasExtent.suggestAxisTypes(augmentedObjectiveCRS, getDisplayDimensions()); |
| getDisplayAxes(axisTypes); |
| } |
| /* |
| * Create the `gridToCRS` transform using the "display to objective" transform augmented with POI |
| * coordinate values in supplemental dimensions. Those coordinate values will be stored in the |
| * translation terms of the `gridToCRS` matrix. |
| */ |
| if (objectiveToDisplay == null) { |
| objectiveToDisplay = createObjectiveToDisplay(); |
| } |
| LinearTransform gridToCRS = objectiveToDisplay.inverse(); |
| if (supplementalDimensions != 0) { |
| gridToCRS = CanvasExtent.createGridToCRS(gridToCRS.getMatrix(), pointOfInterest, supplementalDimensions); |
| } |
| /* |
| * Create the grid extent with a number of dimensions that include the supplemental dimensions. |
| * The cell indices range of all supplemental dimensions is [0 … 0]. If a point of interest is |
| * available, the `GridExtent` will contain the grid coordinates of that point. |
| */ |
| final GridExtent extent; |
| if (displayBounds.isEmpty()) { |
| extent = null; |
| } else { |
| DirectPosition poi = objectivePOI; |
| if (poi != null) { |
| poi = objectiveToDisplay.transform(objectivePOI, null); |
| } |
| extent = CanvasExtent.create(displayBounds, poi, axisTypes, gridToCRS.getSourceDimensions()); |
| } |
| gridGeometry = new GridGeometry(extent, PixelInCell.CELL_CORNER, gridToCRS, augmentedObjectiveCRS); |
| } catch (FactoryException | TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotCompute_1, GRID_GEOMETRY_PROPERTY), e); |
| } |
| return gridGeometry; |
| } |
| |
| /** |
| * Sets canvas properties from the given grid geometry. This convenience method converts the |
| * coordinate reference system, "grid to CRS" transform and extent of the given grid geometry |
| * to {@code Canvas} properties. If the given value is different than the previous value, then |
| * change events are sent to all listeners registered for the {@value #DISPLAY_BOUNDS_PROPERTY}, |
| * {@value #OBJECTIVE_CRS_PROPERTY}, {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} |
| * (with {@link TransformChangeEvent.Reason#GRID_GEOMETRY_CHANGE} reason), |
| * and/or {@value #POINT_OF_INTEREST_PROPERTY} properties, in that order. |
| * |
| * <p>The value given to this method will be returned by {@link #getGridGeometry()} as long as |
| * none of above cited properties is changed. If one of those properties changes (for example |
| * if the user zooms or pans the map), then a new grid geometry will be computed. There is no |
| * guarantee that the recomputed grid geometry will be similar to the grid geometry specified |
| * to this method. For example, the {@link GridExtent} in supplemental dimensions may be different.</p> |
| * |
| * @param newValue the grid geometry from which to get new canvas properties. |
| * @throws RenderException if the given grid geometry cannot be converted to canvas properties. |
| */ |
| public void setGridGeometry(final GridGeometry newValue) throws RenderException { |
| ArgumentChecks.ensureNonNull(GRID_GEOMETRY_PROPERTY, newValue); |
| if (!newValue.equals(gridGeometry)) try { |
| /* |
| * Do not test grid.isDefined(…) — we consider all elements as mandatory for this method. |
| * First, get the dimensions to show in the canvas by searching dimensions having a span |
| * larger than 1 grid cell. Those spans will become the sizes of display bounds. |
| * |
| * Result of this block: DISPLAY_BOUNDS_PROPERTY: newBounds |
| */ |
| final GridExtent extent = newValue.getExtent(); |
| final int[] displayDimensions = extent.getSubspaceDimensions(getDisplayDimensions()); |
| final GeneralEnvelope newBounds = new GeneralEnvelope(getDisplayCRS()); |
| for (int i=0; i<displayDimensions.length; i++) { |
| final int s = displayDimensions[i]; |
| newBounds.setRange(i, extent.getLow(s), Math.incrementExact(extent.getHigh(s))); |
| } |
| /* |
| * Computes the point of interest in the Coordinate Reference System (CRS) of the given grid geometry. |
| * This point will also contain the coordinates in supplemental dimensions (if any), such as vertical |
| * and temporal positions of the slice shown in this canvas. Those supplemental coordinates should be |
| * computed in cell centers. This suggests that we should use PixelInCell.CELL_CENTER transform, but |
| * actually the coordinates returned by `extent.getPointOfInterest()` for [x … x] ranges (span of 1, |
| * as required for supplemental dimensions) already includes a 0.5 fraction digit. |
| * |
| * Result of this block: POINT_OF_INTEREST_PROPERTY: newPOI |
| */ |
| final MathTransform gridToCRS = newValue.getGridToCRS(PixelInCell.CELL_CORNER); |
| final CoordinateReferenceSystem crs; |
| final GeneralDirectPosition newPOI; |
| if (newValue.isDefined(GridGeometry.CRS)) { |
| crs = newValue.getCoordinateReferenceSystem(); |
| newPOI = new GeneralDirectPosition(crs); |
| } else { |
| crs = null; |
| newPOI = new GeneralDirectPosition(gridToCRS.getTargetDimensions()); |
| } |
| gridToCRS.transform(extent.getPointOfInterest(PixelInCell.CELL_CORNER), 0, newPOI.coordinates, 0, 1); |
| /* |
| * Get the CRS component in the dimensions shown by this canvas. |
| * |
| * Result of this block: OBJECTIVE_CRS_PROPERTY: newObjectiveCRS |
| * OBJECTIVE_TO_DISPLAY_PROPERTY: newObjToDisplay |
| */ |
| final TransformSeparator analyzer = new TransformSeparator(gridToCRS, coordinateOperationFactory.getMathTransformFactory()); |
| analyzer.addSourceDimensions(displayDimensions); |
| final LinearTransform newObjectiveToDisplay = MathTransforms.tangent(analyzer.separate().inverse(), newPOI); |
| final int[] objectiveDimensions = analyzer.getTargetDimensions(); |
| final CoordinateReferenceSystem newObjectiveCRS = CRS.selectDimensions(crs, objectiveDimensions); |
| final MathTransform dimensionSelect = MathTransforms.linear( |
| Matrices.createDimensionSelect(newPOI.getDimension(), objectiveDimensions)); |
| /* |
| * At this point we are ready to commit the new values. Before doing so, copy |
| * the current property values in order to provide the old values to listeners. |
| */ |
| final GeneralEnvelope oldBounds = new GeneralEnvelope(displayBounds); |
| final DirectPosition oldPOI = pointOfInterest; |
| final LinearTransform oldObjectiveToDisplay = objectiveToDisplay; |
| final CoordinateReferenceSystem oldObjectiveCRS = objectiveCRS; |
| /* |
| * Set internal fields only after we successfully computed everything, |
| * in order to have a "all or nothing" behavior. |
| */ |
| displayBounds.setEnvelope(newBounds); |
| setObjectiveToDisplayImpl(newObjectiveToDisplay); |
| pointOfInterest = newPOI; |
| objectivePOI = newPOI; |
| objectiveCRS = newObjectiveCRS; |
| multidimToObjective = dimensionSelect; |
| augmentedObjectiveCRS = null; // Will be recomputed when first needed. |
| axisTypes = null; |
| gridGeometry = newValue; |
| /* |
| * Notify listeners only after all properties have been updated. If a listener throws an exception, |
| * other listeners will not be notified but this Canvas will not be corrupted since all the work to |
| * do in this class is already completed. Order matter, it is documented in this method javadoc. |
| */ |
| fireIfChanged(DISPLAY_BOUNDS_PROPERTY, oldBounds, newBounds); |
| fireIfChanged(OBJECTIVE_CRS_PROPERTY, oldObjectiveCRS, newObjectiveCRS); |
| fireIfChanged(/* OBJECTIVE_TO_DISPLAY */ oldObjectiveToDisplay, newObjectiveToDisplay, true); |
| fireIfChanged(POINT_OF_INTEREST_PROPERTY, oldPOI, newPOI); |
| } catch (IncompleteGridGeometryException | CannotEvaluateException | FactoryException | TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1, GRID_GEOMETRY_PROPERTY), e); |
| } |
| } |
| |
| /** |
| * Fires a property change event if the old and new values are not equal. |
| * |
| * @param propertyName name of the property that changed its value. |
| * @param oldValue the old property value (may be {@code null}). |
| * @param newValue the new property value. |
| */ |
| private void fireIfChanged(final String propertyName, final Object oldValue, final Object newValue) { |
| if (!Objects.equals(oldValue, newValue)) { |
| firePropertyChange(propertyName, oldValue, newValue); |
| } |
| } |
| |
| /** |
| * Fires a property change event if the old and new transforms are not equal. |
| * |
| * @param oldValue the old "objective to display" transform. |
| * @param newValue the new transform, or {@code null} for lazy computation. |
| * @param grid {@code true} if the reason is a grid geometry change, or {@code false} if only a CRS change. |
| */ |
| private void fireIfChanged(final LinearTransform oldValue, final LinearTransform newValue, final boolean grid) { |
| if (!Objects.equals(oldValue, newValue)) { |
| firePropertyChange(new TransformChangeEvent(this, oldValue, newValue, |
| grid ? TransformChangeEvent.Reason.GRID_GEOMETRY_CHANGE |
| : TransformChangeEvent.Reason.CRS_CHANGE)); |
| } |
| } |
| |
| /** |
| * Returns the geographic bounding box encompassing the area shown on the display device. |
| * If the {@linkplain #getObjectiveCRS() objective CRS} is not convertible to a geographic CRS, |
| * then this method returns an empty value. |
| * |
| * @return geographic bounding box encompassing the viewed area. |
| * @throws RenderException in an error occurred while computing the geographic area. |
| * |
| * @see #getDisplayBounds() |
| */ |
| public Optional<GeographicBoundingBox> getGeographicArea() throws RenderException { |
| try { |
| return operationContext.getGeographicArea(this); |
| } catch (TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotCompute_1, GEOGRAPHIC_AREA_PROPERTY), e); |
| } |
| } |
| |
| /** |
| * Returns an estimation of the resolution (in metres) at the point of interest. |
| * If the {@linkplain #getObjectiveCRS() objective CRS} is not convertible to a |
| * geographic CRS, then this method returns an empty value. |
| * |
| * @return estimation of the resolution in metres at current point of interest. |
| * @throws RenderException in an error occurred while computing the resolution. |
| */ |
| public OptionalDouble getSpatialResolution() throws RenderException { |
| try { |
| return operationContext.getSpatialResolution(this); |
| } catch (TransformException e) { |
| throw new RenderException(errors().getString(Errors.Keys.CanNotCompute_1, SPATIAL_RESOLUTION_PROPERTY), e); |
| } |
| } |
| |
| /** |
| * Computes the value for {@link #objectiveToGeographic}. The value is not stored by this method for |
| * giving caller a chance to validate other properties before to write them in a "all or nothing" way. |
| * |
| * @param crs the new objective CRS in process of being set by the caller. |
| * @return the conversion from given CRS to geographic CRS, or {@code null} if none. |
| */ |
| private CoordinateOperation objectiveToGeographic(final CoordinateReferenceSystem crs) throws FactoryException { |
| final GeographicCRS geoCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs, false, false); |
| return (geoCRS != null) ? coordinateOperationFactory.createOperation(crs, geoCRS) : null; |
| } |
| |
| /** |
| * Returns the transform from the given source CRS to the given target CRS with precedence for an operation |
| * valid for the geographic area of this canvas. The transform returned by this method for the same pair of |
| * CRS may differ depending on which area is currently visible in the canvas. All requests for a coordinate |
| * operation should invoke this method instead of {@link CRS#findOperation(CoordinateReferenceSystem, |
| * CoordinateReferenceSystem, GeographicBoundingBox)}. |
| * |
| * @param allowDisplayCRS whether the {@code sourceCRS} can be {@link #getDisplayCRS()}. |
| */ |
| private MathTransform findTransform(CoordinateReferenceSystem source, |
| final CoordinateReferenceSystem target, |
| boolean allowDisplayCRS) |
| throws FactoryException, TransformException, RenderException |
| { |
| if (allowDisplayCRS) { |
| allowDisplayCRS = Utilities.equalsIgnoreMetadata(source, displayBounds.getCoordinateReferenceSystem()); |
| } |
| if (allowDisplayCRS) { |
| source = objectiveCRS; |
| } |
| operationContext.refresh(this); |
| MathTransform tr = coordinateOperationFactory.createOperation(source, target, operationContext).getMathTransform(); |
| if (allowDisplayCRS) { |
| tr = MathTransforms.concatenate(getObjectiveToDisplay().inverse(), tr); |
| } |
| return tr; |
| } |
| |
| /** |
| * Allocates a position which can hold a coordinates in objective CRS. |
| * May be overridden by subclasses for a little bit more efficiency. |
| */ |
| DirectPosition allocatePosition() { |
| return new GeneralDirectPosition(objectiveCRS); |
| } |
| |
| /** |
| * Returns the resources bundle for error messages in the locale of this canvas. |
| */ |
| private Errors errors() { |
| return Errors.forLocale(locale); |
| } |
| } |