Class Canvas

java.lang.Object
org.apache.sis.portrayal.Observable
org.apache.sis.portrayal.Canvas
All Implemented Interfaces:
Localized
Direct Known Subclasses:
PlanarCanvas

public class Canvas extends Observable implements Localized
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).

This 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.

A Canvas manages four fundamental properties:

  • The coordinate reference system to use for displaying data.
  • The location of data to display in all dimensions, including the dimensions not shown by the display device (for example time).
  • The size of the display device, in units of the display coordinate system (typically pixels).
  • The conversion from the Coordinate Reference System to the display coordinate system.
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.

Coordinate Reference Systems

There is three Coordinate Reference Systems involved in the rendering of geospatial data:
  1. The data CRS is specific to the data to be displayed. It may be anything convertible to the objective CRS. Different graphic elements may use different data CRS, potentially with a different number of dimensions.
  2. The 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 than the display device (often 2). Its domain of validity should be wide enough for encompassing all data. The CRS.suggestCommonTarget(…) method may be helpful for choosing an objective CRS from a set of data CRS.
  3. The display CRS is the coordinate system of the display device. The 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.

Location of data to display

In addition of above-cited Coordinate Reference Systems, a 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 resolutions will be computed, and the position to keep fixed when scales and rotations are applied.

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.

Example: if some data have (x,y,z) dimensions and other data have (x,y,t) dimensions, then the point of interest shall contain coordinate values for at least all of the (x,y,z,t) 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 (x,y) dimensions (horizontal plane), then the point of interest defines the z value (elevation or depth) and the t value (date and time) of the slice to show.

Display device size

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 getDisplayBounds() as an envelope having the number of dimensions of the display device. The display bounds is usually given in pixel units, but other units such as Units.POINT are also authorized. The zoom level is given indirectly by the getObjectiveToDisplay() transform. The display device may have a wraparound axis, for example in the spherical coordinate system of a planetarium.

Multi-threading

Canvas is not thread-safe. Synchronization, if desired, must be done by the caller. Another common strategy is to interact with Canvas from a single thread, for example the Swing or JavaFX event queue.
Since:
1.1
Version:
1.3
  • Field Details

  • Constructor Details

    • Canvas

      protected Canvas(org.opengis.referencing.crs.EngineeringCRS displayCRS, 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) Units.PIXEL for Cartesian coordinate systems, with Units.DEGREE in polar, cylindrical or spherical coordinate systems.
      Parameters:
      displayCRS - the coordinate system of the display device.
      locale - the locale to use for labels and some messages, or null for default.
  • Method Details

    • getLocale

      public Locale getLocale()
      Returns the locale used for texts or for producing some error messages. May be null if no locale has been specified, in which case the system default should be used.
      Specified by:
      getLocale in interface Localized
      Returns:
      the locale for messages, or null if not explicitly defined.
    • getDisplayDimensions

      int getDisplayDimensions()
      Returns the number of dimensions of the display device. Subclasses may override for a little bit more efficiency.
    • getDisplayAxes

      void getDisplayAxes(org.opengis.metadata.spatial.DimensionNameType[] axisTypes)
      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 Canvas class does not know well the geometry of the display device. It is okay to leave elements to null.
      Parameters:
      axisTypes - where to store the name of display axes. The array length will be at least getDisplayDimensions() (it will often be longer).
    • getDisplayCRS

      public final org.opengis.referencing.crs.EngineeringCRS getDisplayCRS()
      Returns the Coordinate Reference System of the display device. The axis units of measurement are typically (but not necessarily) Units.PIXEL for Cartesian coordinate systems, with 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).
      Usage note: 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 PlanarCanvas, in which case the caller may want more information about the geometry of the display device.

      Note that the CRS.findOperation(…) static method can generally not handle this display CRS. To apply coordinate operations on display coordinates, getObjectiveToDisplay() transform must be inverted and used.

      Returns:
      the Coordinate Reference System of the display device.
      See Also:
    • getObjectiveCRS

      public org.opengis.referencing.crs.CoordinateReferenceSystem getObjectiveCRS()
      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.

      This value may be null on newly created Canvas, before data are added and canvas is configured. It should not be null anymore once a Canvas is ready for displaying.

      Returns:
      the Coordinate Reference System in which to transform all data before displaying.
      See Also:
    • setObjectiveCRS

      public void setObjectiveCRS(org.opengis.referencing.crs.CoordinateReferenceSystem newValue, org.opengis.geometry.DirectPosition anchor) throws RenderException
      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 setGridGeometry(GridGeometry) instead.

      The given CRS should have a domain of validity wide enough for encompassing all data (the 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 "objectiveCRS" property.

      If the transform between old and new CRS is not identity, then this method recomputes the objective to display 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 "objectiveToDisplay" property change event with the TransformChangeEvent.Reason.CRS_CHANGE reason to be sent to listeners. That event is sent after the above-cited "objectiveCRS" event (note that "pointOfInterest" stay unchanged). All those change events are sent only after all property values have been updated to their new values.

      Parameters:
      newValue - the new Coordinate Reference System in which to transform all data before displaying.
      anchor - the point to keep at fixed display coordinates, expressed in any compatible CRS. If null, defaults to point of interest. If non-null, the anchor must be associated to a CRS.
      Throws:
      NullPointerException - if the given CRS is null.
      org.opengis.geometry.MismatchedDimensionException - if the given CRS does not have the number of dimensions of the display device.
      RenderException - if the objective CRS cannot be set to the given value for another reason.
    • orthogonalTangent

      private static org.opengis.referencing.operation.MathTransform orthogonalTangent(org.opengis.referencing.operation.MathTransform newToOld, double[] poiInNew) throws org.opengis.referencing.operation.TransformException, RenderException
      Computes the approximate change from a new objectiveToDisplay to the old one for keeping the Point Of Interest (POI) at the same location. The given newToOld argument is the change as a potentially non-linear transform. The transform returned by this method is a linear approximation of newToOld 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.
      Parameters:
      newToOld - the change as a potentially non-linear transform.
      poiInNew - point of interest in the coordinates of the new objective CRS.
      Returns:
      an approximation of newToOld with only uniform scale, rotation and axis flips.
      Throws:
      org.opengis.referencing.operation.TransformException
      RenderException
      See Also:
    • cps

      private static double cps(MatrixSIS affine, int row)
      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.
    • getObjectiveToDisplay

      public LinearTransform getObjectiveToDisplay()
      Returns the (usually affine) conversion from objective CRS to display coordinate system. The source coordinates shall be in the CRS given by getObjectiveCRS() and the converted coordinates will be in the CRS given by getDisplayCRS().

      The objective to display 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 objective to display conversion are not reflected in the returned transform.

      Returns:
      snapshot of the (usually affine) conversion from objective CRS to display coordinate system (never null).
      See Also:
    • createObjectiveToDisplay

      LinearTransform createObjectiveToDisplay()
      Returns the current objective to display conversion managed by the subclass. This method is invoked only if objectiveToDisplay is null, which may happen either at initialization time or if the subclass uses its own specialized field instead of objectiveToDisplay for managing changes in the zooms or viewed area. This method needs to be overridden only by subclasses using such specialization.
      Returns:
      objective to display conversion created from current value managed by subclass.
      See Also:
    • setObjectiveToDisplay

      public void setObjectiveToDisplay(LinearTransform newValue) throws RenderException
      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 "objectiveToDisplay" property. The event reason is TransformChangeEvent.Reason.ASSIGNMENT.

      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 "pointOfInterest" property however. The point of interest may move outside the view area as a result of this method call.

      Parameters:
      newValue - the new objective to display conversion.
      Throws:
      IllegalArgumentException - if given the transform does not have the expected number of dimensions or is not affine.
      RenderException - if the objective to display transform cannot be set to the given value for another reason.
    • setObjectiveToDisplayImpl

      void setObjectiveToDisplayImpl(LinearTransform newValue)
      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 TransformChangeEvent after all fields are updated. This design choice is because this method is usually invoked as part of a larger set of changes.

      If the new value is null, then this method only declares that the objectiveToDisplay transform became invalid and will need to be recomputed. It is subclasses responsibility to recompute the transform in their createObjectiveToDisplay().

      Parameters:
      newValue - the new "objective to display" transform, or null if it will be computed later by createObjectiveToDisplay(). A null value is okay only when invoked by subclasses that overrode createObjectiveToDisplay().
      See Also:
    • getDisplayBounds

      public org.opengis.geometry.Envelope getDisplayBounds()
      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 getDisplayCRS().

      This value may be null on newly created Canvas, before data are added and canvas is configured. It should not be null anymore once a Canvas is ready for displaying.

      Returns:
      size and location of the display device.
      See Also:
    • setDisplayBounds

      public void setDisplayBounds(org.opengis.geometry.Envelope newValue) throws RenderException
      Sets the size and location of the display device. The envelope CRS shall be either the display CRS or unspecified, in which case the display CRS is assumed. Unit of measurement is typically (but not necessarily) Units.PIXEL. If the given value is different than the previous value, then a change event is sent to all listeners registered for the "displayBounds" property.
      Parameters:
      newValue - the new display bounds.
      Throws:
      IllegalArgumentException - if the given envelope does not have the expected CRS or number of dimensions.
      RenderException - if the display bounds cannot be set to the given value for another reason.
    • getPointOfInterest

      public org.opengis.geometry.DirectPosition getPointOfInterest(boolean objective)
      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.

      The coordinates can be given in their original CRS or in the objective CRS. If objective is 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 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 class javadoc for more discussion.) If objective is true, then the position is transformed to the objective CRS.

      This value is initially null. A value should be specified either by invoking setPointOfInterest(DirectPosition) or setGridGeometry(GridGeometry).

      Parameters:
      objective - whether to return a position transformed to objective CRS.
      Returns:
      coordinates of a representative point, or null if unspecified.
      See Also:
    • setPointOfInterest

      public void setPointOfInterest(org.opengis.geometry.DirectPosition newValue) throws RenderException
      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 "pointOfInterest" property.
      Parameters:
      newValue - the new coordinates of a representative point.
      Throws:
      NullPointerException - if the given position is null.
      IllegalArgumentException - if the given position does not have a CRS.
      RenderException - if the point of interest cannot be set to the given value.
    • getObjectivePOI

      final double[] getObjectivePOI()
      Returns the coordinate values of the Point Of Interest (POI) in objective CRS. The array length should be equal to getDisplayDimensions(). May be null if the point of interest is unknown.
    • getGridGeometry

      public GridGeometry getGridGeometry() throws RenderException
      Returns canvas properties (CRS, display bounds, conversion) encapsulated in a grid geometry. This is a convenience method for interoperability with grid coverage API. If setGridGeometry(GridGeometry) has been invoked with a non-null value and no other Canvas property changed since that method call, then this method returns that value. Otherwise this method computes a grid geometry as described below.

      The set of 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 (x,y) coordinates but the point of interest includes also a t coordinate, then a third dimension (which we call the supplemental dimension) for t is added to the CRS, GridExtent and "grid to CRS" transform of the returned grid geometry.

      Canvas properties → grid geometry properties
      Grid geometry element Display dimensions Supplemental dimensions
      GridGeometry.getCoordinateReferenceSystem() getObjectiveCRS(). Some of getPointOfInterest(false).getCoordinateReferenceSystem()
      GridGeometry.getExtent() getDisplayBounds() rounded to enclosing (floor and ceil) integers [0 … 0]
      GridGeometry.getGridToCRS(PixelInCell) Inverse of getObjectiveToDisplay() Some point of interest coordinates as translation terms
      The GridGeometry.getGridToCRS(PixelInCell) transform built by this method is always a 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.
      Returns:
      a grid geometry encapsulating canvas properties, including supplemental dimensions if possible.
      Throws:
      RenderException - if the grid geometry cannot be computed.
    • setGridGeometry

      public void setGridGeometry(GridGeometry newValue) throws RenderException
      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 Canvas properties. If the given value is different than the previous value, then change events are sent to all listeners registered for the "displayBounds", "objectiveCRS", "objectiveToDisplay" (with TransformChangeEvent.Reason.GRID_GEOMETRY_CHANGE reason), and/or "pointOfInterest" properties, in that order.

      The value given to this method will be returned by 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 GridExtent in supplemental dimensions may be different.

      Parameters:
      newValue - the grid geometry from which to get new canvas properties.
      Throws:
      RenderException - if the given grid geometry cannot be converted to canvas properties.
    • fireIfChanged

      private void fireIfChanged(String propertyName, Object oldValue, Object newValue)
      Fires a property change event if the old and new values are not equal.
      Parameters:
      propertyName - name of the property that changed its value.
      oldValue - the old property value (may be null).
      newValue - the new property value.
    • fireIfChanged

      private void fireIfChanged(LinearTransform oldValue, LinearTransform newValue, boolean grid)
      Fires a property change event if the old and new transforms are not equal.
      Parameters:
      oldValue - the old "objective to display" transform.
      newValue - the new transform, or null for lazy computation.
      grid - true if the reason is a grid geometry change, or false if only a CRS change.
    • getGeographicArea

      public Optional<org.opengis.metadata.extent.GeographicBoundingBox> getGeographicArea() throws RenderException
      Returns the geographic bounding box encompassing the area shown on the display device. If the objective CRS is not convertible to a geographic CRS, then this method returns an empty value.
      Returns:
      geographic bounding box encompassing the viewed area.
      Throws:
      RenderException - in an error occurred while computing the geographic area.
      See Also:
    • getSpatialResolution

      public OptionalDouble getSpatialResolution() throws RenderException
      Returns an estimation of the resolution (in metres) at the point of interest. If the objective CRS is not convertible to a geographic CRS, then this method returns an empty value.
      Returns:
      estimation of the resolution in metres at current point of interest.
      Throws:
      RenderException - in an error occurred while computing the resolution.
    • objectiveToGeographic

      private org.opengis.referencing.operation.CoordinateOperation objectiveToGeographic(org.opengis.referencing.crs.CoordinateReferenceSystem crs) throws org.opengis.util.FactoryException
      Computes the value for objectiveToGeographic(org.opengis.referencing.crs.CoordinateReferenceSystem). 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.
      Parameters:
      crs - the new objective CRS in process of being set by the caller.
      Returns:
      the conversion from given CRS to geographic CRS, or null if none.
      Throws:
      org.opengis.util.FactoryException
    • findTransform

      private org.opengis.referencing.operation.MathTransform findTransform(org.opengis.referencing.crs.CoordinateReferenceSystem source, org.opengis.referencing.crs.CoordinateReferenceSystem target, boolean allowDisplayCRS) throws org.opengis.util.FactoryException, org.opengis.referencing.operation.TransformException, RenderException
      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 CRS.findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, GeographicBoundingBox).
      Parameters:
      allowDisplayCRS - whether the sourceCRS can be getDisplayCRS().
      Throws:
      org.opengis.util.FactoryException
      org.opengis.referencing.operation.TransformException
      RenderException
    • allocatePosition

      org.opengis.geometry.DirectPosition allocatePosition()
      Allocates a position which can hold a coordinates in objective CRS. May be overridden by subclasses for a little bit more efficiency.
    • errors

      private Errors errors()
      Returns the resources bundle for error messages in the locale of this canvas.