| /* |
| * 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); |
| } |
| } |