/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2013, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.display.canvas;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.measure.quantity.Length;
import javax.measure.Unit;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeneralDerivedCRS;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Classes;
import org.apache.sis.referencing.crs.DefaultDerivedCRS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.internal.referencing.provider.Affine;
import org.apache.sis.referencing.operation.DefaultConversion;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.apache.sis.referencing.CRS;
import org.geotoolkit.referencing.operation.matrix.XAffineTransform;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Loggings;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.util.Utilities;
import org.apache.sis.measure.Units;
/**
*
* @author Johann Sorel (Geomatys)
*/
public abstract class AbstractCanvas2D extends AbstractCanvas{
/**
* The operation method used by {@link #getDisplayCRS()}.
* This is a temporary constant, as we will probably need to replace the creation
* of a {@link DefaultDerivedCRS} by something else. After that replacement, this
* constant will be removed.
*/
private static final Affine DISPLAY_TO_OBJECTIVE_OPERATION = new Affine();
public static final class AxisFinder implements Comparator<CoordinateSystemAxis>{
private final CoordinateSystemAxis crs;
public AxisFinder(CoordinateSystemAxis crs) {
this.crs = crs;
}
@Override
public int compare(CoordinateSystemAxis o1, CoordinateSystemAxis o2) {
if(o1.getName().getCode().equals(crs.getName().getCode())){
return 0;
}
return -1;
}
}
/**
* The name of the {@linkplain PropertyChangeEvent property change event} fired when the
* {@linkplain AbstractCanvas2D#getObjectiveCRS canvas crs} changed.
*/
public static final String OBJECTIVE_CRS_KEY = "ObjectiveCRS";
/**
* The name of the {@linkplain PropertyChangeEvent property change event} fired when the
* {@linkplain AbstractCanvas2D#getEnvelope } changed.
*/
public static final String ENVELOPE_KEY = "Envelope";
/**
* The name of the {@linkplain PropertyChangeEvent property change event} fired when the
* {@linkplain AbstractCanvas2D#getObjectiveToDisplay transform} changed.
*/
public static final String TRANSFORM_KEY = "Transform";
/**
* The name of the {@linkplain PropertyChangeEvent property change event} fired when the
* {@linkplain AbstractCanvas2D#getDisplayBounds rectangle} changed.
*/
public static final String BOUNDS_KEY = "Bounds";
/**
* A set of {@link MathTransform}s from various source CRS. The target CRS must be the
* {@linkplain #getObjectiveCRS objective CRS} for all entries. Keys are source CRS.
* This map is used only in order to avoid the costly call to
* {@link CoordinateOperationFactory#createOperation} as much as possible. If a
* transformation is not available in this collection, then the usual factory will be used.
*/
private final transient Map<CoordinateReferenceSystem,MathTransform> transforms = new HashMap<>();
/**
* Contains the canvas bounds.
*/
private final Rectangle2D displayBounds = new Rectangle2D.Double(0,0,1,1);
private final AffineTransform objToDisp = new AffineTransform();
private CoordinateReferenceSystem objectiveCRS;
private CoordinateReferenceSystem objectiveCRS2D;
private double proportion = 1;
private boolean autoRepaint = false;
private GeneralEnvelope envelope;
//navigation constraint
private double minscale = Double.NaN;
private double maxscale = Double.NaN;
public AbstractCanvas2D() {
this(new Hints());
}
public AbstractCanvas2D(Hints hints) {
this(CommonCRS.WGS84.normalizedGeographic(),hints);
}
public AbstractCanvas2D(CoordinateReferenceSystem crs, Hints hints) {
super(hints);
ArgumentChecks.ensureNonNull("Objective CRS", crs);
objectiveCRS = crs;
try {
objectiveCRS2D = CRSUtilities.getCRS2D(objectiveCRS);
} catch (TransformException ex) {
getLogger().log(Level.WARNING, null, ex);
}
envelope = new GeneralEnvelope(objectiveCRS);
}
public CoordinateReferenceSystem getObjectiveCRS() {
return objectiveCRS;
}
public void setObjectiveCRS(final CoordinateReferenceSystem crs) throws TransformException{
ArgumentChecks.ensureNonNull("Objective CRS", crs);
if(Utilities.equalsIgnoreMetadata(objectiveCRS, crs)){
return;
}
//store the visible area to restore it later
GeneralEnvelope preserve = null;
if(!displayBounds.isEmpty()){
preserve = new GeneralEnvelope(envelope);
}
try {
resetTransform();
} catch (NoninvertibleTransformException ex) {
throw new TransformException("Fail to change objective CRS", ex);
}
final CoordinateReferenceSystem oldCRS = objectiveCRS;
objectiveCRS = crs;
envelope = new GeneralEnvelope(objectiveCRS);
objectiveCRS2D = CRSUtilities.getCRS2D(objectiveCRS);
firePropertyChange(OBJECTIVE_CRS_KEY, oldCRS, crs);
if(preserve != null){
//restore previous visible area
GeneralEnvelope env = new GeneralEnvelope(Envelopes.transform(preserve, objectiveCRS2D));
if(!isValid(env)) env = null;
//try to normalize before reproject
if(env == null && preserve.normalize()){
env = new GeneralEnvelope(Envelopes.transform(preserve, objectiveCRS2D));
}
if(!isValid(env)) env = null;
//try to reduce to domain before reproject
if(env == null){
final Envelope domain = org.geotoolkit.referencing.CRS.getEnvelope(preserve.getCoordinateReferenceSystem());
if(domain != null){
preserve.intersect(domain);
env = new GeneralEnvelope(Envelopes.transform(preserve, objectiveCRS2D));
}
}
if(!isValid(env)) env = null;
//fall back on crs domain
if(env == null){
final Envelope domain = org.geotoolkit.referencing.CRS.getEnvelope(objectiveCRS2D);
if(domain!=null){
env = new GeneralEnvelope(domain);
}
}
try {
if(env != null){
setVisibleArea(env);
}else{
//what can we do ?
}
} catch (NoninvertibleTransformException ex) {
throw new TransformException("Fail to change objective CRS", ex);
}
}
}
public CoordinateReferenceSystem getObjectiveCRS2D() {
return objectiveCRS2D;
}
public CoordinateReferenceSystem getDisplayCRS() {
/*
* TODO: will need a way to avoid the cast below. In my understanding, DerivedCRS may not be the appropriate
* CRS to create after all, because in ISO 19111 a DerivedCRS is more than just a base CRS with a math
* transform. A DerivedCRS may also "inherit" some characteritics of the base CRS. For example if the
* base CRS is a VerticalCRS, then the DerivedCRS may also implement VerticalCRS.
*
* I'm not yet sure what should be the appropriate kind of CRS to create here. ImageCRS? EngineeringCRS?
* How to express the relationship to the base CRS is also not yet determined.
*/
final SingleCRS objCRS2D = (SingleCRS) getObjectiveCRS2D();
final Map<String,?> name = Collections.singletonMap(DefaultDerivedCRS.NAME_KEY, "Derived - "+objCRS2D.getName().toString());
final CoordinateReferenceSystem displayCRS = DefaultDerivedCRS.create(name, objCRS2D,
new DefaultConversion(name, DISPLAY_TO_OBJECTIVE_OPERATION, getObjectiveToDisplay(), null),
objCRS2D.getCoordinateSystem());
return displayCRS;
}
/**
* @return a snapshot objective To display transform.
*/
public AffineTransform2D getObjectiveToDisplay() {
return new AffineTransform2D(objToDisp);
}
public AffineTransform2D getDisplayToObjective() throws NoninvertibleTransformException {
return new AffineTransform2D(objToDisp.createInverse());
}
public Rectangle2D getDisplayBounds() {
return displayBounds.getBounds2D();
}
public void setDisplayBounds(Rectangle2D bounds){
ArgumentChecks.ensureNonNull("Display bounds", bounds);
if(bounds.equals(displayBounds)) return;
final Rectangle2D oldRec = displayBounds.getBounds2D();
this.displayBounds.setRect(bounds);
//fire event
firePropertyChange(BOUNDS_KEY, oldRec, bounds.getBounds2D());
}
/**
* Set the proportions support between X and Y axis.
* if prop = Double.NaN then no correction will be applied
* if prop = 1 then one unit in X will be equal to one unit in Y
* else value will mean that prop*Y will be used
*/
public void setAxisProportions(final double prop) {
this.proportion = prop;
}
/**
*
* @return the X/Y proportion
*/
public double getAxisProportions() {
return proportion;
}
public void setAutoRepaint(final boolean autoRepaint) {
this.autoRepaint = autoRepaint;
}
public boolean isAutoRepaint() {
return autoRepaint;
}
/**
* Changes the {@linkplain AffineTransform} by applying a concatenate affine transform.
* The {@code change} transform
* must express a change in logical units, for example, a translation in metres.
*
* @param change The affine transform change, as an affine transform in logical coordinates. If
* {@code change} is the identity transform, then this method does nothing and
* listeners are not notified.
*/
public void applyTransform(AffineTransform change){
if(change.isIdentity()) return;
objToDisp.concatenate(change);
XAffineTransform.roundIfAlmostInteger(objToDisp, EPS);
fixScale:
if(!Double.isNaN(minscale) || !Double.isNaN(maxscale)){
double scale = CanvasUtilities.computeSEScale(envelope, objToDisp, getDisplayBounds().getBounds());
final Point2D center = getDisplayCenter();
if(center== null) break fixScale;
final double centerX = center.getX();
final double centerY = center.getY();
try {
change = objToDisp.createInverse();
} catch (NoninvertibleTransformException ex) {
getLogger().log(Level.WARNING, null, ex);
break fixScale;
}
double correction = Double.NaN;
if(!Double.isNaN(maxscale) && scale>maxscale){
correction = scale/maxscale;
}
if(!Double.isNaN(minscale) && scale<minscale){
correction = scale/minscale;
}
if(!Double.isNaN(correction)){
change.translate(+centerX, +centerY);
change.scale(correction, correction);
change.translate(-centerX, -centerY);
change.concatenate(objToDisp);
objToDisp.concatenate(change);
XAffineTransform.roundIfAlmostInteger(objToDisp, EPS);
}
}
firePropertyChange(TRANSFORM_KEY, null, change);
updateEnvelope();
repaintIfAuto();
}
private void updateEnvelope() {
final Rectangle2D canvasDisplayBounds = getDisplayBounds();
final Rectangle2D canvasObjectiveBounds;
try {
canvasObjectiveBounds = objToDisp.createInverse().createTransformedShape(canvasDisplayBounds).getBounds2D();
} catch (NoninvertibleTransformException ex) {
getLogger().log(Level.SEVERE, "Failed to calculate canvas objective bounds", ex);
return;
}
final Envelope old = new GeneralEnvelope(envelope);
envelope.setRange(0, canvasObjectiveBounds.getMinX(), canvasObjectiveBounds.getMaxX());
envelope.setRange(1, canvasObjectiveBounds.getMinY(), canvasObjectiveBounds.getMaxY());
firePropertyChange(ENVELOPE_KEY, old, envelope.clone());
}
/**
* Change a range in the canvas envelope.
* Can be used to temporal or elevation range of the map.
*/
private void setRange(final int ordinate, final double min, final double max){
if(envelope.getMinimum(ordinate) == min && envelope.getMaximum(ordinate) == max){
//same values
return;
}
final GeneralEnvelope old = new GeneralEnvelope(envelope);
envelope.setRange(ordinate, min, max);
final GeneralEnvelope nw = new GeneralEnvelope(envelope);
repaintIfAuto();
firePropertyChange(ENVELOPE_KEY, old, nw);
}
public final void repaint(){
repaint(displayBounds);
}
public abstract void repaint(Shape area);
private void repaintIfAuto(){
if(autoRepaint){
repaint();
}
}
public abstract Image getSnapShot();
/**
* Set minimum scale do display.
* Scale is in SE scale.
* @param minscale
*/
public void setMinscale(double minscale) {
this.minscale = minscale;
}
/**
* Get minimum scale do display.
* Scale is in SE scale.
* @return min scale
*/
public double getMinscale() {
return minscale;
}
/**
* Set maximum scale do display.
* Scale is in SE scale.
* @param maxscale
*/
public void setMaxscale(double maxscale) {
this.maxscale = maxscale;
}
/**
* Get maximum scale do display.
* Scale is in SE scale.
* @return max scale
*/
public double getMaxscale() {
return maxscale;
}
////////////////////////////////////////////////////////////////////////////
// Next methods are convinient methods which always end up by calling applyTransform(trs)
////////////////////////////////////////////////////////////////////////////
private void setTransform(final AffineTransform transform){
final AffineTransform2D old = getObjectiveToDisplay();
if(!old.equals(transform)){
objToDisp.setTransform(transform);
updateEnvelope();
repaintIfAuto();
}
}
private void resetTransform() throws NoninvertibleTransformException{
resetTransform(new Rectangle(1, 1), true, false);
}
/**
* Reinitializes the affine transform {@link #zoom} in order to cancel any zoom, rotation or
* translation. The argument {@code yAxisUpward} indicates whether the <var>y</var> axis should
* point upwards. The value {@code false} lets it point downwards. This method is offered
* for convenience sake for derived classes which want to redefine {@link #reset()}.
*
* @param canvasBounds Coordinates, in pixels, of the screen space in which to draw.
* This argument will usually be
* <code>{@link #getZoomableBounds(Rectangle) getZoomableBounds}(null)</code>.
* @param yAxisUpward {@code true} if the <var>y</var> axis should point upwards rather than
* downwards.
*/
private void resetTransform(final Rectangle2D preferredArea, final boolean yAxisUpward,
final boolean preserveRotation) throws NoninvertibleTransformException{
final Rectangle canvasBounds = getDisplayBounds().getBounds();
if(canvasBounds.isEmpty()) return;
if(!isValid(preferredArea)) return;
canvasBounds.x = 0;
canvasBounds.y = 0;
final double rotation = -AffineTransforms2D.getRotation(objToDisp);
if (yAxisUpward) {
objToDisp.setToScale(+1, -1);
}else {
objToDisp.setToIdentity();
}
final AffineTransform transform = setVisibleArea(preferredArea, canvasBounds);
objToDisp.concatenate(transform);
if(preserveRotation){
final double centerX = displayBounds.getCenterX();
final double centerY = displayBounds.getCenterY();
final AffineTransform change = objToDisp.createInverse();
change.translate(+centerX, +centerY);
change.rotate(rotation);
change.translate(-centerX, -centerY);
change.concatenate(objToDisp);
XAffineTransform.roundIfAlmostInteger(change, EPS);
objToDisp.concatenate(change);
}
updateEnvelope();
firePropertyChange(TRANSFORM_KEY, null, null);
repaintIfAuto();
}
//convinient method -----------------------------------------
private static boolean isValid(GeneralEnvelope env){
if(env == null) return false;
if(env.isAllNaN() || env.isEmpty()) return false;
if(Double.isInfinite(env.getMinimum(0)) || Double.isInfinite(env.getMinimum(1)) ||
Double.isInfinite(env.getMaximum(0)) || Double.isInfinite(env.getMaximum(1)) ){
return false;
}
return true;
}
/**
* Checks whether the rectangle {@code rect} is valid. The rectangle
* is considered invalid if its length or width is less than or equal to 0,
* or if one of its coordinates is infinite or NaN.
*/
private static boolean isValid(final Rectangle2D rect) {
if (rect == null) {
return false;
}
final double x = rect.getX();
final double y = rect.getY();
final double w = rect.getWidth();
final double h = rect.getHeight();
return (x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY &&
y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY &&
w > 0 && w < Double.POSITIVE_INFINITY &&
h > 0 && h < Double.POSITIVE_INFINITY);
}
/**
* Defines the limits of the visible part, in logical coordinates. This method will modify the
* zoom and the translation in order to display the specified region. If {@link #zoom} contains
* a rotation, this rotation will not be modified.
*
* @param source Logical coordinates of the region to be displayed.
* @param dest Pixel coordinates of the region of the window in which to
* draw (normally {@link #getDisplayBounds()}).
* @param mask A mask to {@code OR} with the {@link #type} for determining which
* kind of transformation are allowed. The {@link #type} is not modified.
* @return Change to apply to the affine transform {@link #zoom}.
* @throws IllegalArgumentException if {@code source} is empty.
*/
private AffineTransform setVisibleArea(final Rectangle2D source, Rectangle2D dest)
throws IllegalArgumentException,NoninvertibleTransformException{
/*
* Verifies the validity of the source rectangle. An invalid rectangle will be rejected.
* However, we will be more flexible for dest since the window could have been reduced by
* the user.
*/
if (!isValid(source)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyRectangle_1, source));
}
if (!isValid(dest)) {
return new AffineTransform();
}
/*
* Converts the destination into logical coordinates. We can then perform
* a zoom and a translation which would put {@code source} in {@code dest}.
*/
dest = AffineTransforms2D.inverseTransform(objToDisp, dest, null);
final double sourceWidth = source.getWidth ();
final double sourceHeight = source.getHeight();
final double destWidth = dest.getWidth ();
final double destHeight = dest.getHeight();
double sx = destWidth / sourceWidth;
double sy = destHeight / sourceHeight;
//switch among the Axis proportions requested
if( Double.isNaN(proportion) ){
//we dont respect X/Y proportions
}else if( proportion == 1){
/*
* Standardizes the horizontal and vertical scales,
* if such a standardization has been requested.
*/
if (sy * sourceWidth < destWidth) {
sx = sy;
} else if (sx * sourceHeight < destHeight) {
sy = sx;
}
}else{
sy = proportion*sx;
}
final AffineTransform change = AffineTransform.getTranslateInstance(dest.getCenterX(),dest.getCenterY());
change.scale(sx,sy);
change.translate(-source.getCenterX(), -source.getCenterY());
XAffineTransform.roundIfAlmostInteger(change, EPS);
return change;
}
/**
* Constructs a transform between two coordinate reference systems. If a
* {@link Hints#COORDINATE_OPERATION_FACTORY} has been provided, then the specified
* {@linkplain CoordinateOperationFactory coordinate operation factory} will be used.
*
* @param sourceCRS The source coordinate reference system.
* @param targetCRS The target coordinate reference system.
* @param sourceClassName The caller class name, for logging purpose only.
* @param sourceMethodName The caller method name, for logging purpose only.
* @return A transform from {@code sourceCRS} to {@code targetCRS}.
* @throws FactoryException if the transform can't be created.
*
* @see DisplayObject#getRenderingHint(java.awt.RenderingHints.Key)
* @see DisplayObject#setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object)
* @see Hints#COORDINATE_OPERATION_FACTORY
*/
public final synchronized MathTransform getMathTransform(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final Class<?> sourceClassName,
final String sourceMethodName)
throws FactoryException {
/*
* Fast check for a very common case. We will use the more general (but slower)
* 'equalsIgnoreMetadata(...)' version implicitly in the call to factory method.
*/
if (sourceCRS == targetCRS) {
return MathTransforms.identity(sourceCRS.getCoordinateSystem().getDimension());
}
MathTransform tr;
/*
* Checks if the math transform is available in the cache. A majority of transformations
* will be from 'graphicCRS' to 'objectiveCRS' to 'displayCRS'. The cache looks for the
* 'graphicCRS' to 'objectiveCRS' transform.
*/
final CoordinateReferenceSystem objectiveCRS = getObjectiveCRS();
final boolean cachedTransform = Utilities.equalsIgnoreMetadata(targetCRS, objectiveCRS);
if (cachedTransform) {
tr = transforms.get(sourceCRS);
if (tr != null) {
return tr;
}
}
/*
* If one of the CRS is a derived CRS, then check if we can use directly its conversion
* from base without using the costly coordinate operation factory. This check is worth
* to be done since it is a very common situation. A majority of transformations will be
* from 'objectiveCRS' to 'displayCRS', which is the case we test first. The converse
* (transformations from 'displayCRS' to 'objectiveCRS') is less frequent and can be
* handled by the 'transform' cache, which is why we let the factory check for it.
*/
if (targetCRS instanceof GeneralDerivedCRS) {
final GeneralDerivedCRS derivedCRS = (GeneralDerivedCRS) targetCRS;
if (Utilities.equalsIgnoreMetadata(sourceCRS, derivedCRS.getBaseCRS())) {
return derivedCRS.getConversionFromBase().getMathTransform();
}
}
/*
* Now that we failed to reuse a pre-existing transform, ask to the factory
* to create a new one. A message is logged in order to trace down the amount
* of coordinate operations created.
*/
final Logger logger = getLogger();
if (logger.isLoggable(Level.FINER)) {
// FINER is the default level for entering, returning, or throwing an exception.
final LogRecord record = Loggings.getResources(Locale.getDefault()).getLogRecord(Level.FINER,
Loggings.Keys.InitializingTransformation_2,
toString(sourceCRS), toString(targetCRS));
record.setSourceClassName (sourceClassName.getName());
record.setSourceMethodName(sourceMethodName);
logger.log(record);
}
tr = CRS.findOperation(sourceCRS, targetCRS, null).getMathTransform();
if (cachedTransform) {
transforms.put(sourceCRS, tr);
}
return tr;
}
/**
* Returns a string representation of a coordinate reference system. This method is
* used for formatting a logging message in {@link #getMathTransform}.
*/
private static String toString(final CoordinateReferenceSystem crs) {
return Classes.getShortClassName(crs) + "[\"" + crs.getName().getCode() + "\"]";
}
////////////////////////////////////////////////////////////////////////////
// Next methods are convinient methods which always end up by calling applyTransform(trs)
////////////////////////////////////////////////////////////////////////////
/**
* Returns the center of the canvas in objective CRS.
*
* @return DirectPosition : center of the canvas
* @throws java.awt.geom.NoninvertibleTransformException
* @throws org.opengis.referencing.operation.TransformException
*/
public DirectPosition getObjectiveCenter() throws NoninvertibleTransformException, TransformException {
final Point2D center = getDisplayCenter();
getObjectiveToDisplay().inverseTransform(center, center);
final GeneralDirectPosition pt = new GeneralDirectPosition(getObjectiveCRS2D());
pt.setCoordinate(center.getX(), center.getY());
return pt;
}
public void setObjectiveCenter(DirectPosition center) throws NoninvertibleTransformException, TransformException, FactoryException {
final DirectPosition oldCenter = getObjectiveCenter();
final CoordinateReferenceSystem candidateCRS = center.getCoordinateReferenceSystem();
if(candidateCRS != null && !Utilities.equalsIgnoreMetadata(candidateCRS, oldCenter.getCoordinateReferenceSystem())){
final MathTransform trs = CRS.findOperation(candidateCRS, oldCenter.getCoordinateReferenceSystem(), null).getMathTransform();
center = trs.transform(center, null);
}
final double diffX = center.getOrdinate(0) - oldCenter.getOrdinate(0);
final double diffY = center.getOrdinate(1) - oldCenter.getOrdinate(1);
translateObjective(diffX, diffY);
}
public Point2D getDisplayCenter() {
final Rectangle2D rect = getDisplayBounds();
return new Point2D.Double(rect.getCenterX(), rect.getCenterY());
}
/**
* @return visible envelope of the canvas, in Objective CRS
*/
public Envelope getVisibleEnvelope() {
return new GeneralEnvelope(envelope);
}
/**
* @return visible envelope of the canvas, in Objective CRS 2D
* @throws org.opengis.referencing.operation.TransformException
*/
public Envelope getVisibleEnvelope2D() throws TransformException {
final CoordinateReferenceSystem objectiveCRS2D = getObjectiveCRS2D();
return Envelopes.transform(getVisibleEnvelope(), objectiveCRS2D);
}
public void rotate(final double r) throws NoninvertibleTransformException {
rotate(r, getDisplayCenter());
}
public void rotate(final double r, final Point2D center) throws NoninvertibleTransformException {
final AffineTransform2D objToDisp = getObjectiveToDisplay();
final AffineTransform change = objToDisp.createInverse();
if (center != null) {
final double centerX = center.getX();
final double centerY = center.getY();
change.translate(+centerX, +centerY);
change.rotate(-r);
change.translate(-centerX, -centerY);
}
change.concatenate(objToDisp);
XAffineTransform.roundIfAlmostInteger(change, EPS);
applyTransform(change);
}
/**
* Change scale by a precise amount.
*
* @param s : multiplication scale factor
* @throws java.awt.geom.NoninvertibleTransformException
*/
public void scale(final double s) throws NoninvertibleTransformException {
scale(s, getDisplayCenter());
}
/**
*
* @param s
* @param center in Display CRS
* @throws NoninvertibleTransformException
*/
public void scale(final double s, final Point2D center) throws NoninvertibleTransformException {
final AffineTransform2D objToDisp = getObjectiveToDisplay();
final AffineTransform change = objToDisp.createInverse();
if (center != null) {
final double centerX = center.getX();
final double centerY = center.getY();
change.translate(+centerX, +centerY);
change.scale(s, s);
change.translate(-centerX, -centerY);
}
change.concatenate(objToDisp);
XAffineTransform.roundIfAlmostInteger(change, EPS);
applyTransform(change);
}
/**
* Translate of x and y amount in display units.
*
* @param x : translation against the X axy
* @param y : translation against the Y axy
* @throws java.awt.geom.NoninvertibleTransformException
*/
public void translateDisplay(final double x, final double y) throws NoninvertibleTransformException {
final AffineTransform2D objToDisp = getObjectiveToDisplay();
final AffineTransform change = objToDisp.createInverse();
change.translate(x, y);
change.concatenate(objToDisp);
XAffineTransform.roundIfAlmostInteger(change, EPS);
applyTransform(change);
}
public void translateObjective(final double x, final double y) throws NoninvertibleTransformException, TransformException {
final Point2D dispCenter = getDisplayCenter();
final DirectPosition center = getObjectiveCenter();
Point2D objCenter = new Point2D.Double(center.getOrdinate(0) + x, center.getOrdinate(1) + y);
objCenter = getObjectiveToDisplay().transform(objCenter, objCenter);
translateDisplay(dispCenter.getX() - objCenter.getX(), dispCenter.getY() - objCenter.getY());
}
/**
* Changes the {@linkplain #AffineTransform} by applying an affine transform. The {@code change} transform
* must express a change in pixel units, for example, a scrolling of 6 pixels toward right.
*
* @param change The zoom change, as an affine transform in pixel coordinates. If
* {@code change} is the identity transform, then this method does nothing
* and listeners are not notified.
*
* @since 2.1
*/
public void transformPixels(final AffineTransform change) {
if (!change.isIdentity()) {
final AffineTransform2D objToDisp = getObjectiveToDisplay();
final AffineTransform logical;
try {
logical = objToDisp.createInverse();
} catch (NoninvertibleTransformException exception) {
throw new IllegalStateException(exception);
}
logical.concatenate(change);
logical.concatenate(objToDisp);
XAffineTransform.roundIfAlmostInteger(logical, EPS);
applyTransform(logical);
}
}
public void setRotation(final double r) throws NoninvertibleTransformException {
double rotation = getRotation();
rotate(rotation - r);
}
public double getRotation() {
return -AffineTransforms2D.getRotation(getObjectiveToDisplay());
}
public void setScale(final double newScale) throws NoninvertibleTransformException {
final double oldScale = XAffineTransform.getScale(getObjectiveToDisplay());
scale(newScale / oldScale);
}
/**
* Returns the current scale factor. A value of 1/100 means that 100 metres
* are displayed as 1 pixel (provided that the logical coordinates of {@code #getArea} are
* expressed in metres). Scale factors for X and Y axes can be computed separately using the
* following equations:
*
* <table cellspacing=3><tr>
* <td width=50%><IMG src="doc-files/scaleX.png"></td>
* <td width=50%><IMG src="doc-files/scaleY.png"></td>
* </tr></table>
*
* @return scale
*/
public double getScale() {
return XAffineTransform.getScale(getObjectiveToDisplay());
}
/**
* Get objective to display transform at canvas center.
* @return AffineTransform
*/
public AffineTransform getCenterTransform(){
final Rectangle2D rect = getDisplayBounds();
final double centerX = rect.getCenterX();
final double centerY = rect.getCenterY();
final AffineTransform trs = new AffineTransform(1, 0, 0, 1, -centerX, -centerY);
final AffineTransform objToDisp = getObjectiveToDisplay().clone();
trs.concatenate(objToDisp);
return trs;
}
/**
* Set objective to display transform at canvas center.
* @param trs
*/
public void setCenterTransform(AffineTransform trs) {
final Rectangle2D rect = getDisplayBounds();
final double centerX = rect.getCenterX();
final double centerY = rect.getCenterY();
final AffineTransform centerTrs = new AffineTransform(1, 0, 0, 1, centerX, centerY);
centerTrs.concatenate(trs);
setTransform(centerTrs);
}
public void setDisplayVisibleArea(final Rectangle2D dipsEnv) {
try {
Shape shp = getObjectiveToDisplay().createInverse().createTransformedShape(dipsEnv);
setVisibleArea(shp.getBounds2D());
} catch (NoninvertibleTransformException ex) {
getLogger().log(Level.WARNING, null, ex);
}
}
public void setVisibleArea(final Envelope env) throws NoninvertibleTransformException, TransformException {
if(env == null) return;
final CoordinateReferenceSystem envCRS = env.getCoordinateReferenceSystem();
if(envCRS == null) return;
final CoordinateReferenceSystem envCRS2D = CRSUtilities.getCRS2D(envCRS);
Envelope env2D = Envelopes.transform(env, envCRS2D);
//check that the provided envelope is in the canvas crs
final CoordinateReferenceSystem canvasCRS2D = getObjectiveCRS2D();
if(!Utilities.equalsIgnoreMetadata(canvasCRS2D,envCRS2D)){
env2D = Envelopes.transform(env2D, canvasCRS2D);
}
//configure the 2D envelope
Rectangle2D rect2D = new Rectangle2D.Double(env2D.getMinimum(0), env2D.getMinimum(1), env2D.getSpan(0), env2D.getSpan(1));
resetTransform(rect2D, true,false);
final CoordinateSystem cs = envCRS.getCoordinateSystem();
//set the extra xis if some exist
int index=0;
List<SingleCRS> dcrss = CRS.getSingleComponents(envCRS);
// Following loop is a temporary hack for decomposing Geographic3D into Geographic2D + ellipsoidal height.
// This is a wrong thing to do according international standards; we will revisit in a future version.
for (int i=dcrss.size(); --i >= 0;) {
SingleCRS crs = dcrss.get(i);
SingleCRS hcrs = CRS.getHorizontalComponent(crs);
if (hcrs != null && hcrs != crs) {
SingleCRS vcrs = CRS.getVerticalComponent(envCRS, true);
if (vcrs != null && hcrs.getCoordinateSystem().getDimension()
+ vcrs.getCoordinateSystem().getDimension()
== crs.getCoordinateSystem().getDimension())
{
dcrss = new ArrayList<>(dcrss);
dcrss.set(i, hcrs);
dcrss.add(i+1, vcrs);
}
}
}
for(CoordinateReferenceSystem dcrs : dcrss){
if(dcrs.getCoordinateSystem().getDimension()==1){
final CoordinateSystemAxis axis = dcrs.getCoordinateSystem().getAxis(0);
final AxisFinder finder = new AxisFinder(axis);
final int cindex = getAxisIndex(finder);
if(cindex>=0){
setAxisRange(env.getMinimum(index), env.getMaximum(index), finder, dcrs);
}
}
index += dcrs.getCoordinateSystem().getDimension();
}
// for(int i=0, n= cs.getDimension(); i<n;i++){
// final CoordinateSystemAxis axis = cs.getAxis(i);
// final AxisDirection ad = axis.getDirection();
// if(ad.equals(AxisDirection.FUTURE) || ad.equals(AxisDirection.PAST)){
// //found a temporal axis
// final double minT = env.getMinimum(i);
// final double maxT = env.getMaximum(i);
// setTemporalRange(toDate(minT), toDate(maxT));
// } else if(ad.equals(AxisDirection.UP) || ad.equals(AxisDirection.DOWN)){
// //found a vertical axis
// final double minT = env.getMinimum(i);
// final double maxT = env.getMaximum(i);
// //todo should use the axis unit
// setElevationRange(minT, maxT, Units.METRE);
// }
// }
}
/**
* Defines the limits of the visible part, in logical coordinates. This method will modify the
* zoom and the translation in order to display the specified region. If {@link #zoom} contains
* a rotation, this rotation will not be modified.
*
* @param logicalBounds Logical coordinates of the region to be displayed.
* @throws IllegalArgumentException if {@code source} is empty.
* @throws java.awt.geom.NoninvertibleTransformException
*/
public void setVisibleArea(final Rectangle2D logicalBounds) throws IllegalArgumentException, NoninvertibleTransformException {
resetTransform(logicalBounds, true,true);
}
/**
* Set the scale, in a ground unit manner, relation between map display size
* and real ground unit meters;
* @param scale
* @throws org.opengis.referencing.operation.TransformException
*/
public void setGeographicScale(final double scale) throws TransformException {
double currentScale = getGeographicScale();
double factor = currentScale / scale;
try {
scale(factor);
} catch (NoninvertibleTransformException ex) {
getLogger().log(Level.WARNING, null, ex);
}
}
/**
* Returns the geographic scale, in a ground unit manner, relation between map display size
* and real ground unit meters.
*
* @return
* @throws org.opengis.referencing.operation.TransformException
* @throws IllegalStateException If the affine transform used for conversion is in
* illegal state.
*/
public double getGeographicScale() throws TransformException {
return CanvasUtilities.getGeographicScale(getDisplayCenter(), getObjectiveToDisplay(), getObjectiveCRS2D());
}
public void setTemporalRange(final Date startDate, final Date endDate) throws TransformException {
try {
int index = getTemporalAxisIndex();
if (index < 0 && (startDate != null || endDate != null)) {
//no temporal axis, add one
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = appendCRS(crs, CommonCRS.Temporal.JAVA.crs());
setObjectiveCRS(crs);
index = getTemporalAxisIndex();
}
if (index >= 0) {
if(startDate!=null || endDate!=null){
setRange(index,
(startDate!=null)?startDate.getTime():Double.NEGATIVE_INFINITY,
(endDate!=null)?endDate.getTime():Double.POSITIVE_INFINITY);
}else{
//remove this dimension
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = removeCRS(crs, CommonCRS.Temporal.JAVA.crs());
setObjectiveCRS(crs);
}
}
} catch (FactoryException ex) {
throw new TransformException("", ex);
}
}
public Date[] getTemporalRange() {
final int index = getTemporalAxisIndex();
if (index >= 0) {
final Envelope envelope = getVisibleEnvelope();
final Date[] range = new Date[2];
final double min = envelope.getMinimum(index);
final double max = envelope.getMaximum(index);
range[0] = Double.isInfinite(min) ? null : new Date((long)min);
range[1] = Double.isInfinite(max) ? null : new Date((long)max);
return range;
}
return null;
}
public void setElevationRange(final Double min, final Double max, final Unit<Length> unit) throws TransformException {
try {
int index = getElevationAxisIndex();
if(index < 0 && (min!=null || max!=null)){
//no elevation axis, add one
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = appendCRS(crs, CommonCRS.Vertical.ELLIPSOIDAL.crs());
setObjectiveCRS(crs);
index = getElevationAxisIndex();
}
if (index >= 0) {
if(min!=null || max!=null){
setRange(index,
(min!=null)?min:Double.NEGATIVE_INFINITY,
(max!=null)?max:Double.POSITIVE_INFINITY);
}else{
//remove this dimension
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = removeCRS(crs, CommonCRS.Vertical.ELLIPSOIDAL.crs());
setObjectiveCRS(crs);
}
}
} catch (FactoryException ex) {
throw new TransformException("", ex);
}
}
public Double[] getElevationRange() {
final int index = getElevationAxisIndex();
if (index >= 0) {
final Envelope envelope = getVisibleEnvelope();
return new Double[]{envelope.getMinimum(index), envelope.getMaximum(index)};
}
return null;
}
public Unit<Length> getElevationUnit() {
final int index = getElevationAxisIndex();
if (index >= 0) {
return (Unit<Length>) getObjectiveCRS().getCoordinateSystem().getAxis(index).getUnit();
}
return null;
}
//convinient methods -------------------------------------------------
/**
* Find the elevation axis index or -1 if there is none.
*/
private int getElevationAxisIndex() {
final CoordinateReferenceSystem objCrs = getObjectiveCRS();
final CoordinateSystem cs = objCrs.getCoordinateSystem();
for (int i = 0, n = cs.getDimension(); i < n; i++) {
final AxisDirection direction = cs.getAxis(i).getDirection();
final Unit unit = cs.getAxis(i).getUnit();
if (direction == AxisDirection.UP || direction == AxisDirection.DOWN && (unit != null && unit.isCompatible(Units.METRE))) {
return i;
}
}
return -1;
}
/**
* Find the temporal axis index or -1 if there is none.
*/
private int getTemporalAxisIndex() {
final CoordinateReferenceSystem objCrs = getObjectiveCRS();
final CoordinateSystem cs = objCrs.getCoordinateSystem();
for (int i = 0, n = cs.getDimension(); i < n; i++) {
final AxisDirection direction = cs.getAxis(i).getDirection();
if (direction == AxisDirection.FUTURE || direction == AxisDirection.PAST) {
return i;
}
}
return -1;
}
public Double[] getAxisRange(final Comparator<CoordinateSystemAxis> comparator) {
final int index = getAxisIndex(comparator);
if (index >= 0) {
final Envelope envelope = getVisibleEnvelope();
return new Double[]{envelope.getMinimum(index), envelope.getMaximum(index)};
}
return null;
}
public void setAxisRange(final Double min, final Double max,
final Comparator<CoordinateSystemAxis> comparator, CoordinateReferenceSystem axisCrs) throws TransformException {
try {
int index = getAxisIndex(comparator);
if(index < 0 && (min!=null || max!=null)){
//no elevation axis, add one
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = appendCRS(crs, axisCrs);
setObjectiveCRS(crs);
index = getElevationAxisIndex();
}
if (index >= 0) {
if(min!=null || max!=null){
setRange(index,
(min!=null)?min:Double.NEGATIVE_INFINITY,
(max!=null)?max:Double.POSITIVE_INFINITY);
}else{
//remove this dimension
CoordinateReferenceSystem crs = getObjectiveCRS();
crs = removeCRS(crs, axisCrs);
setObjectiveCRS(crs);
}
}
} catch(FactoryException ex) {
throw new TransformException("", ex);
}
}
/**
* Search an axis index.
* Comparator must return 0 when found.
*
* @param comparator
* @return -1 if not found
*/
public int getAxisIndex(final Comparator<CoordinateSystemAxis> comparator) {
final CoordinateReferenceSystem objCrs = getObjectiveCRS();
final CoordinateSystem cs = objCrs.getCoordinateSystem();
for (int i = 0, n = cs.getDimension(); i < n; i++) {
final CoordinateSystemAxis axi = cs.getAxis(i);
if(comparator.compare(axi, axi) == 0) return i;
}
return -1;
}
private CoordinateReferenceSystem appendCRS(final CoordinateReferenceSystem crs, final CoordinateReferenceSystem toAdd) throws FactoryException{
if(crs instanceof CompoundCRS){
final CompoundCRS orig = (CompoundCRS) crs;
final List<CoordinateReferenceSystem> lst = new ArrayList<>(orig.getComponents());
lst.add(toAdd);
return new GeodeticObjectBuilder().addName(orig.getName().getCode())
.createCompoundCRS(lst.toArray(new CoordinateReferenceSystem[lst.size()]));
} else {
return new GeodeticObjectBuilder().addName(crs.getName().getCode() + ' ' + toAdd.getName().getCode())
.createCompoundCRS(crs, toAdd);
}
}
private CoordinateReferenceSystem removeCRS(final CoordinateReferenceSystem crs, final CoordinateReferenceSystem toRemove) throws FactoryException{
if(crs instanceof CompoundCRS){
final CompoundCRS orig = (CompoundCRS) crs;
final List<CoordinateReferenceSystem> lst = new ArrayList<>(orig.getComponents());
lst.remove(toRemove);
if(lst.size() == 1){
return lst.get(0);
}
return new GeodeticObjectBuilder().addName(orig.getName().getCode())
.createCompoundCRS(lst.toArray(new CoordinateReferenceSystem[lst.size()]));
}else{
return crs;
}
}
}