| /* |
| * 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.gui.map; |
| |
| import java.util.function.Predicate; |
| import javafx.concurrent.Task; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.SingleCRS; |
| import org.opengis.referencing.datum.PixelInCell; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.coverage.grid.GridCoverage; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.gui.coverage.CoverageCanvas; |
| import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.util.logging.Logging; |
| import static org.apache.sis.gui.internal.LogHandler.LOGGER; |
| |
| |
| /** |
| * Finds a coordinate operation between two CRS in the context of a {@link MapCanvas}. |
| * The operation may depend on the region visible in the canvas and the resolution. |
| * Computing the coordinate operation may be costly, and for this reason should be |
| * done in a background thread. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| abstract class OperationFinder extends Task<MathTransform> { |
| /** |
| * The grid geometry of data, or {@code null} if none or unknown. This is used for getting the operation between |
| * two CRSs when one of the source or target CRS is {@link org.apache.sis.referencing.CommonCRS.Engineering#GRID}. |
| * Because the relationship from {@code GRID} CRS to a geospatial CRS is unknown to {@code CRS.findOperation(…)}, |
| * the operation cannot be found without the help of this {@code dataGeometry} field. |
| * |
| * Actually this information is rarely needed. It is needed only if there is no known affine transform from grid |
| * coordinates to some geospatial coordinates that we can use as a starting point (before to apply map projection |
| * to other CRSs if desired). For example, some netCDF files provides the coordinates of each pixel in data arrays. |
| * Those data arrays can be stored (indirectly) in this {@code dataGeometry} object. |
| */ |
| private final GridGeometry dataGeometry; |
| |
| /** |
| * Region visible in the map canvas, or {@code null} if none. May be in any CRS. |
| */ |
| private final Envelope areaOfInterest; |
| |
| /** |
| * The source and target CRS requested by the user. This is usually the {@link #operation} source |
| * and target CRS, unless a CRS was a {@linkplain #isGridCRS(CoordinateReferenceSystem) grid CRS}. |
| */ |
| private final CoordinateReferenceSystem sourceCRS, targetCRS; |
| |
| /** |
| * {@code true} if {@link #sourceCRS} or {@link #targetCRS} is a |
| * {@linkplain #isGridCRS(CoordinateReferenceSystem) grid CRS}. |
| */ |
| private boolean sourceIsGrid, targetIsGrid; |
| |
| /** |
| * The coordinate operation from {@link #sourceCRS} to {@link #targetCRS}, computed in background thread. |
| * The {@link CoordinateOperation#getMathTransform()} value may not be the complete transform returned by |
| * {@link #getValue()} because the latter may include transform from/to {@linkplain #isGridCRS grid CRS}. |
| */ |
| private CoordinateOperation operation; |
| |
| /** |
| * Creates a new task for finding the coordinate operation between two CRS. |
| * |
| * @param canvas the canvas for which we are searching a coordinate operation, or {@code null}. |
| * @param areaOfInterest region visible in the map canvas, or {@code null}. May be in any CRS. |
| * @param sourceCRS source CRS of the transform to compute. |
| * @param targetCRS target CRS of the transform to compute. |
| */ |
| protected OperationFinder(final MapCanvas canvas, |
| final Envelope areaOfInterest, |
| final CoordinateReferenceSystem sourceCRS, |
| final CoordinateReferenceSystem targetCRS) |
| { |
| this.dataGeometry = dataGeometry(canvas); |
| this.sourceCRS = sourceCRS; |
| this.targetCRS = targetCRS; |
| this.areaOfInterest = areaOfInterest; |
| } |
| |
| /** |
| * Returns the <em>data</em> (not canvas) grid geometry, or {@code null} if none. |
| */ |
| private static GridGeometry dataGeometry(final MapCanvas canvas) { |
| if (canvas instanceof CoverageCanvas) { |
| final GridCoverage coverage = ((CoverageCanvas) canvas).getCoverage(); |
| if (coverage != null) { |
| return coverage.getGridGeometry(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Computes the transform from the source CRS to the target CRS specified at construction time. |
| * This method is invoked in a background thread and does not need to be invoked explicitly. |
| */ |
| @Override |
| protected MathTransform call() throws Exception { |
| DefaultGeographicBoundingBox bbox = null; |
| if (areaOfInterest != null) try { |
| bbox = new DefaultGeographicBoundingBox(); |
| bbox.setBounds(areaOfInterest); |
| } catch (TransformException e) { |
| bbox = null; |
| Logging.recoverableException(LOGGER, getCallerClass(), getCallerMethod(), e); |
| } |
| MathTransform before = null; |
| MathTransform after = null; |
| CoordinateReferenceSystem source = sourceCRS; |
| CoordinateReferenceSystem target = targetCRS; |
| if (dataGeometry != null && dataGeometry.isDefined(GridGeometry.CRS | GridGeometry.GRID_TO_CRS)) { |
| if (sourceIsGrid = isGridCRS(source)) { |
| before = dataGeometry.getGridToCRS(PixelInCell.CELL_CENTER); |
| source = dataGeometry.getCoordinateReferenceSystem(); |
| } |
| if (targetIsGrid = isGridCRS(target)) { |
| after = dataGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse(); |
| target = dataGeometry.getCoordinateReferenceSystem(); |
| } |
| } |
| operation = CRS.findOperation(source, target, bbox); |
| MathTransform transform = operation.getMathTransform(); |
| if (before != null) transform = MathTransforms.concatenate(before, transform); |
| if (after != null) transform = MathTransforms.concatenate(transform, after); |
| return transform; |
| } |
| |
| /** |
| * If the given CRS is a grid CRS, replaces it by a geospatial CRS if possible. |
| * If the given CRS is not geospatial, then this method tries to replace it by |
| * the CRS of the coverage shown by the canvas (this is not necessarily the |
| * {@linkplain MapCanvas#getObjectiveCRS() objective CRS}). |
| * |
| * @param crs the CRS to eventually replace by a geospatial CRS. |
| * @param canvas the canvas that this status bar is tracking. |
| */ |
| static CoordinateReferenceSystem toGeospatial(CoordinateReferenceSystem crs, final MapCanvas canvas) { |
| if (isGridCRS(crs)) { |
| final GridGeometry dataGeometry = dataGeometry(canvas); |
| if (dataGeometry != null && dataGeometry.isDefined(GridGeometry.CRS)) { |
| crs = dataGeometry.getCoordinateReferenceSystem(); |
| } |
| } |
| return crs; |
| } |
| |
| /** |
| * Returns {@code true} if the given coordinate reference system is the CRS of the grid. |
| * We use the {@link org.apache.sis.referencing.CommonCRS.Engineering#GRID} datum as a signature. |
| */ |
| private static boolean isGridCRS(final CoordinateReferenceSystem crs) { |
| return (crs instanceof SingleCRS) && CommonCRS.Engineering.GRID.datum().equals(((SingleCRS) crs).getDatum()); |
| } |
| |
| /** |
| * Returns the coordinate operation computed by the {@link #call()} method. The associated transform can be |
| * obtained by {@link #getValue()}. The {@link CoordinateOperation#getMathTransform()} method should not be |
| * used because it may be incomplete if the source or target CRS was a grid CRS. |
| */ |
| public final CoordinateOperation getOperation() { |
| return operation; |
| } |
| |
| /** |
| * Returns the target CRS, giving precedence to {@link CoordinateOperation#getTargetCRS()} if suitable. |
| * That precedence is because the {@link CoordinateOperation} may provide a more complete CRS from EPSG |
| * database. |
| */ |
| public final CoordinateReferenceSystem getTargetCRS() { |
| if (!targetIsGrid) { |
| final CoordinateReferenceSystem crs = operation.getTargetCRS(); |
| if (crs != null) return crs; |
| } |
| return targetCRS; |
| } |
| |
| /** |
| * Returns a predicate for determining if {@link OperationFinder} task need to be executed again. |
| * If there is no need to perform such check, returns {@code null}. |
| * |
| * <p><b>Note:</b> actually recomputing everything is a bit overly aggresive. We could keep the |
| * {@link CoordinateOperation} found by {@link #call()} and just update the {@link MathTransform} |
| * before or after the operation. But the use of a grid CRS should be rare enough that it is not |
| * worth to do this optimization.</p> |
| * |
| * @see StatusBar#fullOperationSearchRequired |
| */ |
| final Predicate<MapCanvas> fullOperationSearchRequired() { |
| return (sourceIsGrid | targetIsGrid) ? new UpdateCheck(dataGeometry) : null; |
| } |
| |
| /** |
| * The predicate for determining if {@link OperationFinder} task needs to be executed again. |
| */ |
| private static final class UpdateCheck implements Predicate<MapCanvas> { |
| private final GridGeometry dataGeometry; |
| |
| UpdateCheck(final GridGeometry dataGeometry) { |
| this.dataGeometry = dataGeometry; |
| } |
| |
| @Override public boolean test(final MapCanvas canvas) { |
| return !dataGeometry.equals(dataGeometry(canvas)); |
| } |
| } |
| |
| /** |
| * Returns the class to report as the caller in case of non-fatal error. This is used for logging. |
| */ |
| protected abstract Class<?> getCallerClass(); |
| |
| /** |
| * Returns the method to report as the caller in case of non-fatal error. This is used for logging. |
| */ |
| protected abstract String getCallerMethod(); |
| } |