blob: 1b4fa71d943866f4a9e576b7c118aef3232b0cd8 [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.storage.aggregate;
import java.time.Instant;
import java.time.Duration;
import org.apache.sis.storage.Resource;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.referencing.privy.ExtentSelector;
import org.apache.sis.util.privy.Strings;
/**
* Algorithm to apply when more than one grid coverage can be found at the same grid index.
* A merge may happen if an aggregated coverage is created with {@link CoverageAggregator},
* and the extent of some source coverages are overlapping in the dimension to aggregate.
*
* <h2>Example</h2>
* A collection of {@link GridCoverage} instances may represent the same phenomenon
* (for example Sea Surface Temperature) over the same geographic area but at different dates and times.
* {@link CoverageAggregator} can be used for building a single data cube with a time axis.
* But if two coverages have overlapping time ranges, and if a user request data in the overlapping region,
* then the aggregated coverages have more than one source coverages capable to provide the requested data.
* This enumeration specify how to handle this multiplicity.
*
* <h2>Default behavior</h2>
* If no merge strategy is specified, then the default behavior is to throw
* {@link SubspaceNotSpecifiedException} when the {@link GridCoverage#render(GridExtent)} method
* is invoked and more than one source coverage (slice) is found for a specified grid index.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.3
* @since 1.3
*/
public final class MergeStrategy {
/**
* Selects a single slice using criteria based first on temporal extent, then on geographic area.
* This default instance do not use any duration.
*
* @see #selectByTimeThenArea(Duration)
*/
private static final MergeStrategy SELECT_BY_TIME = new MergeStrategy(null);
/**
* Temporal granularity of the time of interest, or {@code null} if none.
* If non-null, intersections with TOI will be rounded to an integer number of this granularity.
* This is useful if data are expected at an approximately regular interval
* and we want to ignore slight variations in the temporal extent declared for each image.
*/
private final Duration timeGranularity;
/**
* Creates a new merge strategy. This constructor is private for now because
* we have not yet decided a callback API for custom merges.
*/
private MergeStrategy(final Duration timeGranularity) {
this.timeGranularity = timeGranularity;
}
/**
* Selects a single slice using criteria based first on temporal extent, then on geographic area.
* This strategy applies the following rules, in order:
*
* <ol>
* <li>Slice having largest intersection with the time of interest (TOI) is selected.</li>
* <li>If two or more slices have the same intersection with TOI,
* then the one with less "overtime" (time outside TOI) is selected.</li>
* <li>If two or more slices are considered equal after above criteria,
* then the one best centered on the TOI is selected.</li>
* </ol>
*
* <div class="note"><b>Rational:</b>
* the "smallest time outside" criterion (rule 2) is before "best centered" criterion (rule 3)
* because of the following scenario: if a user specifies a "time of interest" (TOI) of 1 day
* and if there are two slices intersecting the TOI, with one slice being a raster of monthly
* averages the other slice being a raster of daily data, we want the daily data to be selected
* even if by coincidence the monthly averages is better centered.</div>
*
* If the {@code timeGranularity} argument is non-null, then intersections with TOI will be rounded
* to an integer number of the specified granularity and the last criterion in above list is relaxed.
* This is useful when data are expected at an approximately regular time interval (for example one remote
* sensing image per day) and we want to ignore slight variations in the temporal extent declared for each image.
*
* <p>If there is no time of interest, or the slices do not declare time range,
* or some slices are still at equality after application of above criteria,
* then the selection continues on the basis of geographic criteria:</p>
*
* <ol>
* <li>Largest intersection with the area of interest (AOI) is selected.</li>
* <li>If two or more slices have the same intersection area with AOI, then the one with the less
* "irrelevant" material is selected. "Irrelevant" material are area outside the AOI.</li>
* <li>If two or more slices are considered equal after above criteria,
* the one best centered on the AOI is selected.</li>
* <li>If two or more slices are considered equal after above criteria,
* then the first of those candidates is selected.</li>
* </ol>
*
* If two slices are still considered equal after all above criteria, then an arbitrary one is selected.
*
* <h4>Limitations</h4>
* Current implementation does not check the vertical dimension.
* This check may be added in a future version.
*
* @param timeGranularity the temporal granularity of the Time of Interest (TOI), or {@code null} if none.
* @return a merge strategy for selecting a slice based on temporal criteria first.
*/
public static MergeStrategy selectByTimeThenArea(final Duration timeGranularity) {
return (timeGranularity != null) ? new MergeStrategy(timeGranularity) : SELECT_BY_TIME;
}
/**
* Applies the merge using the strategy represented by this instance.
* Current implementation does only a slice selection.
* A future version may allow real merge operations.
*
* @param request the geographic area and temporal extent requested by user.
* @param candidates grid geometry of all slices that intersect the request. Null elements are ignored.
* @return index of best slice according the heuristic rules of this {@code MergeStrategy}.
*/
final Integer apply(final GridGeometry request, final GridGeometry[] candidates) {
final ExtentSelector<Integer> selector = new ExtentSelector<>(
request.getGeographicExtent().orElse(null),
request.getTemporalExtent());
if (timeGranularity != null) {
selector.setTimeGranularity(timeGranularity);
selector.alternateOrdering = true;
}
for (int i=0; i < candidates.length; i++) {
final GridGeometry candidate = candidates[i];
if (candidate != null) {
final Instant[] t = candidate.getTemporalExtent();
final int n = t.length;
selector.evaluate(candidate.getGeographicExtent().orElse(null),
(n == 0) ? null : t[0],
(n == 0) ? null : t[n-1], i);
}
}
return selector.best();
}
/**
* Returns a resource with same data as specified resource but using this merge strategy.
* If the given resource is an instance created by {@link CoverageAggregator} and uses a different strategy,
* then a new resource using this merge strategy is returned. Otherwise the given resource is returned as-is.
* The returned resource will share the same resources and caches than the given resource.
*
* @param resource the resource for which to update the merge strategy, or {@code null}.
* @return resource with updated merge strategy, or {@code null} if the given resource was null.
*/
public Resource apply(final Resource resource) {
if (resource instanceof AggregatedResource) {
return ((AggregatedResource) resource).apply(this);
}
return resource;
}
/**
* Returns a string representation of this strategy for debugging purposes.
*
* @return string representation of this strategy.
*/
@Override
public String toString() {
return Strings.toString(getClass(), "algo", "selectByTimeThenArea", "timeGranularity", timeGranularity);
}
}