blob: c5c74815ee16c54547acc7f8433412ee4582fedb [file] [log] [blame]
/*
* 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.coverage.grid;
import java.util.Map;
import java.util.TreeMap;
import java.awt.image.RenderedImage;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.image.DataType;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.coverage.internal.MultiSourceArgument;
import org.apache.sis.util.internal.CollectionsExt;
/**
* A grid coverage where each band (sample dimension) is taken from a selection of bands
* in a sequence of source coverages.
*
* <h2>Restrictions</h2>
* <ul>
* <li>All coverages shall have the same {@linkplain GridCoverage#getGridGeometry() domain}, except for
* the grid extent and the translation terms which can vary by integer amounts of grid cells.</li>
* <li>All grid extents shall intersect and the intersection area shall be non-empty.</li>
* <li>If coverage data are stored in {@link RenderedImage} instances,
* then all images shall use the same data type.</li>
* </ul>
*
* @author Alexis Manin (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
final class BandAggregateGridCoverage extends GridCoverage {
/**
* The source grid coverages.
*/
private final GridCoverage[] sources;
/**
* The sample dimensions to use for each source coverage, in order.
* The length of this array is always equal to {@link #sources} array length.
* This array may contain {@code null} elements but never contain empty element.
*/
private final int[][] bandsPerSource;
/**
* Total number of bands.
*/
private final int numBands;
/**
* Translations in units of grid cells to apply on an extent of this grid coverage
* for getting a "grid to CRS" transform compatible with each source.
*/
private final long[][] gridTranslations;
/**
* Index of a sources having the same "grid to CRS" than this grid coverage, or -1 if none.
*/
private final int sourceOfGridToCRS;
/**
* The data type identifying the primitive type used for storing sample values in each band.
*/
private final DataType dataType;
/**
* The processor to use for creating images.
* The processor {@linkplain ImageProcessor#getColorizer() colorizer}
* will determine the color model applied on the aggregated images.
*/
private final ImageProcessor processor;
/**
* Creates a new band aggregated coverage from the given sources.
*
* @param aggregate the source grid coverages together with bands to select.
* @param processor the processor to use for creating images.
* @throws IllegalArgumentException if there is an incompatibility between some source coverages
* or if some band indices are duplicated or outside their range of validity.
*/
BandAggregateGridCoverage(final MultiSourceArgument<GridCoverage> aggregate, final ImageProcessor processor) {
super(aggregate.domain(GridCoverage::getGridGeometry), aggregate.ranges());
this.sources = aggregate.sources();
this.bandsPerSource = aggregate.bandsPerSource(true);
this.numBands = aggregate.numBands();
this.gridTranslations = aggregate.gridTranslations();
this.sourceOfGridToCRS = aggregate.sourceOfGridToCRS();
this.processor = processor;
this.dataType = sources[0].getBandType();
for (int i=1; i < sources.length; i++) {
final GridCoverage source = sources[i];
final DataType type = source.getBandType();
if (!dataType.equals(type)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedDataType));
}
}
}
/**
* Returns potentially deeper sources than the user supplied coverage.
* This method unwraps {@link BandAggregateGridCoverage} for making possible to detect that
* two consecutive coverages are actually the same coverage, with only different bands selected.
*
* @param unwrapper a handler where to supply the result of an aggregate decomposition.
*/
static void unwrap(final MultiSourceArgument<GridCoverage>.Unwrapper unwrapper) {
if (unwrapper.source instanceof BandAggregateGridCoverage) {
final var aggregate = (BandAggregateGridCoverage) unwrapper.source;
unwrapper.applySubset(aggregate.sources, aggregate.bandsPerSource, GridCoverage::getSampleDimensions);
}
}
/**
* Returns the data type identifying the primitive type used for storing sample values in each band.
*/
@Override
DataType getBandType() {
return dataType;
}
/**
* Returns a two-dimensional slice of grid data as a rendered image.
* This operation is potentially costly if the {@code sliceExtent} argument changes often because
* the previously computed images are unlikely to be reused if the coordinate systems are different.
* It may result in the same bands being copied may times in different {@link RenderedImage} instances.
*
* <h4>Implementation note</h4>
* We do not compute the rendered image in advance (which would have produced better caching) because
* the image to cache depends on {@code sliceExtent} if this coverage has more than two dimensions.
*
* @param sliceExtent a subspace of this grid coverage where all dimensions except two have a size of 1 cell.
* @return the grid slice as a rendered image. Image location is relative to {@code sliceExtent}.
*/
@Override
public RenderedImage render(GridExtent sliceExtent) {
/*
* We need a non-null extent for making sure that all rendered images
* will use coordinates relative to the same extent.
*/
if (sliceExtent == null) {
sliceExtent = gridGeometry.getExtent();
}
final RenderedImage[] images = new RenderedImage[sources.length];
for (int i=0; i<images.length; i++) {
images[i] = sources[i].render(sliceExtent.translate(gridTranslations[i]));
}
return processor.aggregateBands(images, bandsPerSource);
}
/**
* Creates a new function for computing or interpolating sample values at given locations.
*/
@Override
public Evaluator evaluator() {
return new CombinedEvaluator(sources);
}
/**
* An evaluator which delegates to the evaluators of all source grid coverages.
* This class is not thread-safe.
*
* <h2>Implementation note</h2>
* We need this specialized class instead of relying on the default implementation inherited
* from {@link GridCoverage} because the latter may invoke {@link #render(GridExtent)} often,
* which is potentially costly. Furthermore, it may not work well with coverages having more
* than two dimensions.
*/
private final class CombinedEvaluator implements Evaluator {
/**
* Evaluators from all source coverages.
*/
private final Evaluator[] sources;
/**
* The union of slice coordinates from all sources, or {@code null} if not yet computed.
* All coverages should have the same values, but we nevertheless compute union in case.
*/
private Map<Integer,Long> slices;
/**
* Creates a new evaluator which will delegate to the evaluators of all given sources.
*/
CombinedEvaluator(final GridCoverage[] coverages) {
sources = new Evaluator[coverages.length];
for (int i=0; i < coverages.length; i++) {
sources[i] = coverages[i].evaluator();
}
}
/**
* Returns the coverage that created this evaluator.
*/
@Override
public GridCoverage getCoverage() {
return BandAggregateGridCoverage.this;
}
/**
* Returns the default slice where to perform evaluation, or an empty map if unspecified.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public Map<Integer,Long> getDefaultSlice() {
if (slices == null) {
final var c = new TreeMap<Integer,Long>();
for (final Evaluator source : sources) {
c.putAll(source.getDefaultSlice());
}
slices = CollectionsExt.unmodifiableOrCopy(c);
}
return slices;
}
/**
* Sets the default slice where to perform evaluation, or an empty map if unspecified.
*/
@Override
public void setDefaultSlice(final Map<Integer, Long> slice) {
slices = null;
for (final Evaluator source : sources) {
source.setDefaultSlice(slice);
}
}
/**
* Returns whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
*/
@Override
public boolean isNullIfOutside() {
for (final Evaluator source : sources) {
if (!source.isNullIfOutside()) {
return false;
}
}
return true;
}
/**
* Sets whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds.
*/
@Override
public void setNullIfOutside(final boolean flag) {
for (final Evaluator source : sources) {
source.setNullIfOutside(flag);
}
}
/**
* Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the coverage.
*/
@Override
public boolean isWraparoundEnabled() {
for (final Evaluator source : sources) {
if (!source.isWraparoundEnabled()) {
return false;
}
}
return true;
}
/**
* Sets whether this evaluator is allowed to wraparound coordinates that are outside the coverage.
*/
@Override
public void setWraparoundEnabled(final boolean allow) {
for (final Evaluator source : sources) {
source.setWraparoundEnabled(allow);
}
}
/**
* Returns a sequence of double values for a given point in the coverage.
* This method delegates to all source evaluators and merge the results.
*/
@Override
public double[] apply(final DirectPosition point) {
final double[] aggregate = new double[numBands];
int offset = 0;
for (int i=0; i < sources.length; i++) {
final double[] values = sources[i].apply(point);
final int[] bands = bandsPerSource[i];
if (bands == null) {
System.arraycopy(values, 0, aggregate, offset, values.length);
offset += values.length;
} else {
for (int b : bands) {
values[offset++] = values[b];
}
}
}
return aggregate;
}
/**
* Converts the specified geospatial position to grid coordinates.
*/
@Override
public FractionalGridCoordinates toGridCoordinates(DirectPosition point) throws TransformException {
if (sourceOfGridToCRS >= 0) {
return sources[sourceOfGridToCRS].toGridCoordinates(point);
} else {
throw new UnsupportedOperationException();
}
}
}
}