| /* |
| * 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.storage.geotiff; |
| |
| import java.util.List; |
| import java.util.Arrays; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import org.opengis.util.NameSpace; |
| import org.opengis.util.FactoryException; |
| import org.opengis.geometry.DirectPosition; |
| import org.apache.sis.coverage.grid.PixelInCell; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.TransformException; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.apache.sis.coverage.grid.GridExtent; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.coverage.grid.GridCoverage; |
| import org.apache.sis.storage.GridCoverageResource; |
| import org.apache.sis.storage.DataStore; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.DataStoreReferencingException; |
| import org.apache.sis.storage.base.StoreResource; |
| import org.apache.sis.storage.base.GridResourceWrapper; |
| import org.apache.sis.storage.base.ResourceOnFileSystem; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.privy.DirectPositionView; |
| import org.apache.sis.referencing.operation.matrix.MatrixSIS; |
| |
| |
| /** |
| * A list of Image File Directory (FID) where the first entry is the image at finest resolution |
| * and following entries are images at finer resolutions. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| final class MultiResolutionImage extends GridResourceWrapper implements ResourceOnFileSystem, StoreResource { |
| /** |
| * Name of the image at finest resolution. |
| * This is used as the namespace for overviews. |
| */ |
| private NameSpace namespace; |
| |
| /** |
| * Descriptions of each <i>Image File Directory</i> (IFD) in the GeoTIFF file. |
| * Should have at least 2 elements. The full-resolution image shall be at index 0. |
| */ |
| private final ImageFileDirectory[] levels; |
| |
| /** |
| * Resolutions (in units of CRS axes) of each level from finest to coarsest resolution. |
| * Array elements may be {@code null} if not yet computed. |
| * |
| * @see #resolution(int) |
| * @see #getResolutions() |
| */ |
| private final double[][] resolutions; |
| |
| /** |
| * The last coordinate operation returned by {@link #getTransformFrom(CoordinateReferenceSystem)}. |
| * Used as an optimization in the common case where the same CRS is used for many requests. |
| */ |
| private volatile CoordinateOperation lastOperation; |
| |
| /** |
| * Creates a multi-resolution images with all the given reduced-resolution (overview) images, |
| * from finest resolution to coarsest resolution. The full-resolution image shall be at index 0. |
| */ |
| MultiResolutionImage(final List<ImageFileDirectory> overviews) { |
| levels = overviews.toArray(ImageFileDirectory[]::new); |
| resolutions = new double[levels.length][]; |
| } |
| |
| /** |
| * Returns the data store that produced this resource. |
| */ |
| @Override |
| public final DataStore getOriginator() { |
| return levels[0].getOriginator(); |
| } |
| |
| /** |
| * Gets the paths to files used by this resource, or an empty array if unknown. |
| */ |
| @Override |
| public Path[] getComponentFiles() { |
| return levels[0].getComponentFiles(); |
| } |
| |
| /** |
| * Returns the object on which to perform all synchronizations for thread-safety. |
| */ |
| @Override |
| protected final Object getSynchronizationLock() { |
| return levels[0].getSynchronizationLock(); |
| } |
| |
| /** |
| * Creates the resource on which to delegate operations. |
| * The source is the first image, the one having finest resolution. |
| * By Cloud Optimized GeoTIFF (COG) convention, this is the image containing metadata (CRS). |
| * This method is invoked in a synchronized block when first needed and the result is cached. |
| */ |
| @Override |
| protected GridCoverageResource createSource() throws DataStoreException { |
| try { |
| return getImageFileDirectory(0); |
| } catch (IOException e) { |
| throw levels[0].reader.store.errorIO(e); |
| } |
| } |
| |
| /** |
| * Completes and returns the image at the given pyramid level. |
| * Indices are in the same order as the images appear in the TIFF file, |
| * with 0 for the full resolution image. |
| * |
| * @param index image index (level) in the pyramid, with 0 for finest resolution. |
| * @return image at the given pyramid level. |
| */ |
| private ImageFileDirectory getImageFileDirectory(final int index) throws IOException, DataStoreException { |
| assert Thread.holdsLock(getSynchronizationLock()); |
| final ImageFileDirectory dir = levels[index]; |
| if (dir.hasDeferredEntries) { |
| dir.reader.resolveDeferredEntries(dir); |
| } |
| if (dir.validateMandatoryTags() && index != 0) { |
| if (namespace == null) { |
| final ImageFileDirectory base = levels[0]; |
| // Identifier should never be empty (see `DataCube.getIdentifier()` contract). |
| namespace = base.reader.nameFactory.createNameSpace(base.getIdentifier().get(), null); |
| } |
| dir.setOverviewIdentifier(namespace, index); |
| } |
| return dir; |
| } |
| |
| /** |
| * Returns the resolution (in units of CRS axes) for the given level. |
| * |
| * @param level the desired resolution level, numbered from finest to coarsest resolution. |
| * @return resolution at the specified level, not cloned (caller shall not modify). |
| */ |
| private double[] resolution(final int level) throws DataStoreException { |
| double[] resolution = resolutions[level]; |
| if (resolution == null) try { |
| final ImageFileDirectory image = getImageFileDirectory(level); |
| final ImageFileDirectory base = getImageFileDirectory(0); |
| final GridGeometry geometry = base.getGridGeometry(); |
| final GridExtent fullExtent = geometry.getExtent(); |
| final GridExtent subExtent = image.getExtent(); |
| final double[] scales = new double[fullExtent.getDimension()]; |
| for (int i=0; i<scales.length; i++) { |
| scales[i] = fullExtent.getSize(i, false) / subExtent.getSize(i, false); |
| } |
| image.initReducedResolution(base, scales); |
| if (geometry.isDefined(GridGeometry.GRID_TO_CRS)) { |
| DirectPosition poi = new DirectPositionView.Double(fullExtent.getPointOfInterest(PixelInCell.CELL_CENTER)); |
| MatrixSIS gridToCRS = MatrixSIS.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER).derivative(poi)); |
| resolution = gridToCRS.multiply(scales); |
| } else { |
| // Assume an identity transform for the `gridToCRS` of full resolution image. |
| resolution = scales; |
| } |
| for (int i=0; i<resolution.length; i++) { |
| resolution[i] = Math.abs(resolution[i]); |
| } |
| resolutions[level] = resolution; |
| } catch (TransformException e) { |
| throw new DataStoreReferencingException(e.getMessage(), e); |
| } catch (IOException e) { |
| throw levels[level].reader.store.errorIO(e); |
| } |
| return resolution; |
| } |
| |
| /** |
| * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store. |
| * Elements are ordered from finest (smallest numbers) to coarsest (largest numbers) resolution. |
| */ |
| @Override |
| public List<double[]> getResolutions() throws DataStoreException { |
| final double[][] copy = new double[resolutions.length][]; |
| synchronized (getSynchronizationLock()) { |
| for (int i=0; i<copy.length; i++) { |
| copy[i] = resolution(i).clone(); |
| } |
| } |
| return Arrays.asList(copy); |
| } |
| |
| /** |
| * Converts a resolution from units in the given CRS to units of this coverage CRS. |
| * |
| * @param domain the geometry from which to get the resolution. |
| * @return resolution from the given grid geometry in units of this coverage CRS, or {@code null}. |
| */ |
| private double[] getResolution(final GridGeometry domain) throws DataStoreException { |
| if (domain == null || !domain.isDefined(GridGeometry.RESOLUTION)) { |
| return null; |
| } |
| double[] resolution = domain.getResolution(true); |
| if (domain.isDefined(GridGeometry.CRS | GridGeometry.ENVELOPE)) try { |
| final CoordinateReferenceSystem crs = domain.getCoordinateReferenceSystem(); |
| CoordinateOperation op = lastOperation; |
| if (op == null || !crs.equals(op.getTargetCRS())) { |
| final GridGeometry gg = getGridGeometry(); |
| op = CRS.findOperation(crs, gg.getCoordinateReferenceSystem(), gg.getGeographicExtent().orElse(null)); |
| lastOperation = op; |
| } |
| final MathTransform sourceToCoverage = op.getMathTransform(); |
| if (!sourceToCoverage.isIdentity()) { |
| /* |
| * If the `domain` grid geometry has a resolution and an envelope, then it should have |
| * an extent and a "grid to CRS" transform (otherwise it may be a `GridGeometry` bug) |
| */ |
| DirectPosition poi = new DirectPositionView.Double(domain.getExtent().getPointOfInterest(PixelInCell.CELL_CENTER)); |
| poi = domain.getGridToCRS(PixelInCell.CELL_CENTER).transform(poi, null); |
| final MatrixSIS derivative = MatrixSIS.castOrCopy(sourceToCoverage.derivative(poi)); |
| resolution = derivative.multiply(resolution); |
| for (int i=0; i<resolution.length; i++) { |
| resolution[i] = Math.abs(resolution[i]); |
| } |
| } |
| } catch (FactoryException | TransformException e) { |
| throw new DataStoreReferencingException(e.getMessage(), e); |
| } |
| return resolution; |
| } |
| |
| /** |
| * Loads a subset of the grid coverage represented by this resource. |
| * |
| * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. |
| * @param ranges 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all. |
| * @return the grid coverage for the specified domain and ranges. |
| * @throws DataStoreException if an error occurred while reading the grid coverage data. |
| */ |
| @Override |
| public GridCoverage read(final GridGeometry domain, final int... ranges) throws DataStoreException { |
| final double[] request = getResolution(domain); |
| int level = (request != null) ? resolutions.length : 1; |
| synchronized (getSynchronizationLock()) { |
| finer: while (--level > 0) { |
| final double[] resolution = resolution(level); |
| for (int i=0; i<request.length; i++) { |
| if (!(request[i] >= resolution[i])) { // Use `!` for catching NaN. |
| continue finer; |
| } |
| } |
| break; |
| } |
| final ImageFileDirectory image; |
| try { |
| image = getImageFileDirectory(level); |
| } catch (IOException e) { |
| throw levels[level].reader.store.errorIO(e); |
| } |
| image.setLoadingStrategy(getLoadingStrategy()); |
| return image.read(domain, ranges); |
| } |
| } |
| } |