/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2007-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.coverage.sql; import java.util.Locale; import java.util.Date; import java.util.Set; import java.util.HashSet; import java.util.SortedSet; import java.util.Map; import java.util.List; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.sql.SQLException; import java.awt.Color; import java.awt.geom.Dimension2D; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.InvalidObjectException; import java.lang.reflect.InvocationTargetException; import javax.measure.IncommensurableException; import org.opengis.util.FactoryException; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.geometry.MismatchedReferenceSystemException; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.Localized; import org.geotoolkit.util.Utilities; import org.geotoolkit.util.DateRange; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.util.logging.Logging; import org.geotoolkit.util.collection.FrequencySortedSet; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.geotoolkit.internal.sql.table.DefaultEntry; import org.geotoolkit.internal.sql.table.TablePool; import org.geotoolkit.internal.sql.table.SpatialDatabase; import org.geotoolkit.internal.sql.table.NoSuchRecordException; import org.apache.sis.internal.metadata.AxisDirections; import org.geotoolkit.internal.UnmodifiableArraySortedSet; import org.geotoolkit.coverage.Category; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.io.CoverageStoreException; import org.geotoolkit.coverage.grid.GeneralGridEnvelope; import org.geotoolkit.coverage.grid.GeneralGridGeometry; import org.geotoolkit.internal.EmptySortedSet; import org.apache.sis.referencing.crs.DefaultTemporalCRS; import org.apache.sis.referencing.operation.matrix.MatrixSIS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; import org.geotoolkit.resources.Errors; import static org.geotoolkit.util.collection.XCollections.unmodifiableOrCopy; /** * A layer of {@linkplain GridCoverage grid coverages} sharing common properties. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.12 * * @since 3.10 (derived from Seagis) * @module */ final class LayerEntry extends DefaultEntry implements Layer, Localized { /** * For cross-version compatibility. */ private static final long serialVersionUID = 5283559646740856038L; /** * Typical time interval (in days) between images, or {@link Double#NaN} if unknown. * For example a layer of weekly <cite>Sea Surface Temperature</cite> (SST) coverages * may set this field to 7, while a layer of mounthly SST coverage may set this field * to 30. The value is only approximative. * * @todo We should compute it automatically instead. */ final double timeInterval; /** * The domain for this layer, or {@code null} if not yet computed. Will be computed * only when first needed, and is serialized because its computation recquires a * connection to the database. * * @see #getDomain() */ private volatile DomainOfLayerEntry domain; /** * The series associated with their identifiers. This map will be created only when * first needed. This field is not declared {@code volatile} because the method that * compute it needs to be synchronized anyway. * * @see #getSeriesMap() */ private Map<Integer,SeriesEntry> series; /** * How many coverages are found in each series, sorted by decreasing frequency. * This is computed when first needed. This field is not declared {@code volatile} * because the method that compute it needs to be synchronized anyway. * * @see #getCountBySeries() */ private FrequencySortedSet<SeriesEntry> countBySeries; /** * How many coverages are found for each grid geometry, sorted by decreasing frequency. * This is computed when first needed. This field is not declared {@code volatile} * because the method that compute it needs to be synchronized anyway. * * @see #getCountByExtent() */ private FrequencySortedSet<GridGeometryEntry> countByExtent; /** * A fallback layer to be used if no image can be found for a given date in this layer. * May be {@code null} if there is no fallback. * <p> * Upon construction, this field contains only the layer name as a {@link String}. * This is converted to {@link LayerEntry} only when first needed. * * @see #getFallback() */ private volatile Object fallback; /** * The typical resolution along each axis of the database CRS. This is computed only * when first needed. This field is serialized because its computation requires an * access to the database. * * @see #getTypicalResolution() */ private double[] resolution; /** * The set of available dates. Will be computed by when first needed. This field * is serialized because its computation requires a connection to the database. * * @see #getAvailableTimes() */ private SortedSet<Date> availableTimes; /** * The set of available altitudes. Will be computed when first needed. This field doesn't * need to be serialized since it can be recomputed from the {@linkplain #countByExtent}, * which is serialized. * * @see #getAvailableElevations() */ private transient SortedSet<Number> availableElevations; /** * Caches the value returned by {@link #getSampleValueRanges()}. Computed only when first * needed. This field doesn't need to be serialized since it can be recomputed from the * {@linkplain #series} field, which is serialized. * * @see #getSampleValueRanges() */ private transient List<MeasurementRange<?>> sampleValueRanges; /** * Caches the value returned by {@link #getGridGeometries()}. This field doesn't need * to be serialized since it can be recomputed from the {@linkplain #countByExtent}, * which is serialized. * * @see #getGridGeometries() */ private transient SortedSet<GeneralGridGeometry> gridGeometries; /** * Whatever this layer has tiles, or {@code null} if not yet determined. * This method is for {@link GridCoverageTable#createEntry} usage only; * it is not used by this {@code LayerEntry}. */ transient volatile Boolean isTiled; /** * The envelope for all coverages in this layer. Will be computed when first needed. * This field is serialized because its computation requires a connection to the database. * * @see #getEnvelope(Date, Number) */ private volatile CoverageEnvelope coverageEnvelope; /** * The geographic bounding box. Will be computed when first needed. */ private GeographicBoundingBox boundingBox; /** * The {@link TableFactory} or the {@link CoverageDatabase} for fetching table dependencies. * This field is not serialized. It will be {@code null} on deserialization, which * imply that the various {@code getCoverageReference} methods will not be available. * * @see #getTableFactory() * @see #setCoverageDatabase(CoverageDatabase) */ private transient Object tables; /** * Creates a new layer. * * @param name The layer name. * @param timeInterval Typical time interval (in days) between images, or {@link Double#NaN} if unknown. * @param fallback The layer on which to fallback, or {@code null} if none. * @param remarks Optional remarks, or {@code null}. * @param tables The table factory. */ LayerEntry(final Comparable<?> name, final double timeInterval, final String fallback, final String remarks, final TableFactory tables) { super(name, remarks); this.tables = tables; this.timeInterval = timeInterval; } /** * Replaces the {@link #tables} dependency by a dependency toward the database. * This will give us an access to the listeners. */ final void setCoverageDatabase(final CoverageDatabase database) { assert (tables == database) || (tables instanceof TableFactory); tables = database; } /** * Returns the {@link CoverageDatabase} associated with this entry, * or {@code null} if none. */ @Override public CoverageDatabase getCoverageDatabase() { final Object tables = this.tables; if (tables instanceof CoverageDatabase) { return (CoverageDatabase) tables; } return null; } /** * Returns the table factory, or thrown an exception if there is none. * The exception may happen if this entry has been deserialized. * * @return The table factory. * @throws IllegalStateException If this entry is not connected to a database. */ private TableFactory getTableFactory() throws IllegalStateException { final TableFactory tb; final Object tables = this.tables; if (tables instanceof CoverageDatabase) { tb = ((CoverageDatabase) tables).database; } else { tb = (TableFactory) tables; } if (tb == null) { throw new IllegalStateException(errors().getString(Errors.Keys.NoDataSource)); } return tb; } /** * Returns the locale to use for formatting messages. * * @since 3.16 */ @Override public Locale getLocale() { final TableFactory tb; final Object tables = this.tables; if (tables instanceof CoverageDatabase) { tb = ((CoverageDatabase) tables).database; } else { tb = (TableFactory) tables; } return (tb != null) ? tb.getLocale() : null; } /** * Returns the resources bundle for error messages. */ private Errors errors() { return Errors.getResources(getLocale()); } /** * Returns the name of this layer. */ @Override public String getName() { return identifier.toString(); } /** * Returns the domain of this layer, or {@code null} if none. This is not a big deal if * this method is executed twice concurrently, because {@code Table.getEntry(Comparable)} * has its own synchronization lock on its shared cache. * * @throws SQLException If an error occurred while fetching the domain. */ private DomainOfLayerEntry getDomain() throws SQLException { DomainOfLayerEntry entry = domain; if (entry == null) { final String name = getName(); final DomainOfLayerTable domains = getTableFactory().getTable(DomainOfLayerTable.class); try { entry = domains.getEntry(name); } catch (NoSuchRecordException exception) { entry = DomainOfLayerEntry.NULL; Logging.recoverableException(null, LayerEntry.class, "getDomain", exception); } domains.release(); domain = entry; } return entry; } /** * Returns all series in this layer. * * @throws SQLException If an error occurred while fetching the series. */ final Collection<SeriesEntry> getSeries() throws SQLException { return getSeriesMap().values(); } /** * Returns the series for the given identifier, or {@code null} if none. * * @param name The series identifier. * @return The series in this layer for the given identifier, or {@code null} if none. * @throws SQLException If an error occurred while fetching the series. */ final SeriesEntry getSeries(int identifier) throws SQLException { return getSeriesMap().get(identifier); } /** * Returns all series in this layer as (<var>identifier</var>, <var>series</var>) pairs. * * @throws SQLException If an error occurred while fetching the series. */ private synchronized Map<Integer,SeriesEntry> getSeriesMap() throws SQLException { Map<Integer,SeriesEntry> map = series; if (map == null) { final String name = getName(); final SeriesTable st = getTableFactory().getTable(SeriesTable.class); st.setLayer(name); map = unmodifiableOrCopy(st.getEntriesMap()); st.release(); series = map; } return map; } /** * A layer to use as a fallback if no data is available in this layer for a given position. For * example if no data is available in a weekly averaged <cite>Sea Surface Temperature</cite> * (SST) coverage because a location is masked by clouds, we may want to look in the mounthly * averaged SST coverage as a fallback. * * {@section Synchronization note} * This is not a big deal if this method is executed twice concurrently, because * {@code Table.getEntry(Comparable)} has its own synchronization lock on their * shared cache so we will get the same {@code LayerEntry} instance anyway. * * @return The fallback layer, or {@code null} if none. * @throws CoverageStoreException If an error occurred while fetching the fallback. */ @Override public LayerEntry getFallback() throws CoverageStoreException { Object fb = fallback; if (fb instanceof String) { final String name = (String) fb; final TablePool<LayerTable> pool = getTableFactory().layers; try { final LayerTable table = pool.acquire(); fb = table.getEntry(name); pool.release(table); } catch (SQLException e) { throw new CoverageStoreException(e); } fallback = fb; } return (LayerEntry) fb; } /** * Returns the number of coverages in this layer. */ @Override public int getCoverageCount() throws CoverageStoreException { final int[] count; try { count = getCountBySeries().frequencies(); } catch (SQLException e) { throw new CoverageStoreException(e); } int n = 0; for (int i=0; i<count.length; i++) { n += count[i]; } return n; } /** * Returns the number of coverages in each series. This method returns a direct * reference to the internal set - <strong>do not modify!</strong>. */ final synchronized FrequencySortedSet<SeriesEntry> getCountBySeries() throws SQLException { FrequencySortedSet<SeriesEntry> count = countBySeries; if (count == null) { final Map<Integer,SeriesEntry> seriesMap = getSeriesMap(); final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.envelope.clear(); table.setLayerEntry(this); count = new FrequencySortedSet<>(true); for (final SeriesEntry series : seriesMap.values()) { final Map<Integer,Integer> countMap = table.count(series, false); for (final Map.Entry<Integer,Integer> e : countMap.entrySet()) { count.add(seriesMap.get(e.getKey()), e.getValue()); } } pool.release(table); countBySeries = count; } return count; } /** * Returns the number of coverages for each extent. This method returns a direct * reference to the internal set - <strong>do not modify!</strong>. */ final synchronized FrequencySortedSet<GridGeometryEntry> getCountByExtent() throws SQLException { FrequencySortedSet<GridGeometryEntry> count = countByExtent; if (count == null) { final Collection<SeriesEntry> allSeries = getSeries(); final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.envelope.clear(); table.setLayerEntry(this); final GridGeometryTable geometries = table.getGridGeometryTable(); count = new FrequencySortedSet<>(true); for (final SeriesEntry series : allSeries) { final Map<Integer,Integer> countMap = table.count(series, true); for (final Map.Entry<Integer,Integer> e : countMap.entrySet()) { count.add(geometries.getEntry(e.getKey()), e.getValue()); } } pool.release(table); countByExtent = count; } return count; } /** * Returns a time range encompassing all coverages in this layer, or {@code null} if none. * * @return The time range encompassing all coverages, or {@code null}. * @throws CoverageStoreException if an error occurred while fetching the time range. */ @Override public DateRange getTimeRange() throws CoverageStoreException { final DomainOfLayerEntry domain; try { domain = getDomain(); } catch (SQLException e) { throw new CoverageStoreException(e); } return (domain != null) ? domain.timeRange : null; } /** * Returns the set of dates when a coverage is available. */ @Override public synchronized SortedSet<Date> getAvailableTimes() throws CoverageStoreException { SortedSet<Date> available = availableTimes; if (available == null) try { final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.envelope.clear(); table.setLayerEntry(this); available = table.getAvailableTimes(); pool.release(table); availableTimes = available; } catch (SQLException e) { throw new CoverageStoreException(e); } return available; } /** * Returns the set of altitudes where a coverage is available. */ @Override public synchronized SortedSet<Number> getAvailableElevations() throws CoverageStoreException { SortedSet<Number> available = availableElevations; if (available == null) { final Set<GridGeometryEntry> count; try { count = getCountByExtent(); } catch (SQLException e) { throw new CoverageStoreException(e); } available = EmptySortedSet.INSTANCE; if (count != null) { final Set<Double> all = new HashSet<>(); for (final GridGeometryEntry entry : count) { final double[] ordinates = entry.getVerticalOrdinates(); if (ordinates != null) { for (final double z : ordinates) { all.add(z); } } } if (!all.isEmpty()) { available = new UnmodifiableArraySortedSet.Number(all); } } availableElevations = available; } return available; } /** * Returns the ranges of valid <cite>geophysics</cite> values for each band. If some * coverages found in this layer have different range of values, then this method * returns the union of their ranges. * * @return The range of valid sample values. * @throws CoverageStoreException If an error occurred while computing the ranges. */ @Override public synchronized List<MeasurementRange<?>> getSampleValueRanges() throws CoverageStoreException { List<MeasurementRange<?>> sampleValueRanges = this.sampleValueRanges; if (sampleValueRanges == null) try { MeasurementRange<?>[] ranges = null; for (final SeriesEntry series : getSeries()) { final FormatEntry format = series.format; if (format != null) { final MeasurementRange<Double>[] candidates = format.getSampleValueRanges(); if (candidates != null) { if (ranges == null) { ranges = candidates; } else if (!Arrays.equals(ranges, candidates)) { final int length; if (candidates.length <= ranges.length) { length = candidates.length; } else { length = ranges.length; ranges = Arrays.copyOf(ranges, candidates.length); System.arraycopy(candidates, length, ranges, length, candidates.length - length); } try { for (int i=0; i<length; i++) { // TODO: Cast may fail if range is fully included in candidate. ranges[i] = (MeasurementRange<?>) ranges[i].unionAny(candidates[i]); } } catch (IllegalArgumentException e) { // May occurs if the units are not convertible. // We are interested in the IncommensurableException cause. final Throwable cause = e.getCause(); throw new CoverageStoreException(e.getLocalizedMessage(), (cause instanceof Exception) ? (Exception) cause : e); } } } } } if (ranges != null) { sampleValueRanges = UnmodifiableArrayList.wrap(ranges); } else { sampleValueRanges = Collections.emptyList(); } this.sampleValueRanges = sampleValueRanges; } catch (SQLException e) { throw new CoverageStoreException(e); } return sampleValueRanges; } /** * Creates a color ramp for the coverages in this layer. * See the super class javadoc for more details. * <p> * This method requires the {@code geotk-display} module to be on the classpath. * * @since 3.16 */ @Override public RenderedImage getColorRamp(final int band, final MeasurementRange<?> range, final Map<String,?> properties) throws CoverageStoreException, IllegalArgumentException { final Collection<SeriesEntry> series; try { series = getSeries(); } catch (SQLException e) { throw new CoverageStoreException(e); } /* * Search for a suitable quantitative category. If more than one category is suitable, * the one which more closely match the requested range will be selected. If no category * were found because of mismatched units, the first unit exception will be rethrown. */ Category category = null; double categoryFit = 0; Exception conversionError = null; for (final SeriesEntry entry : series) { final FormatEntry format = entry.format; if (format != null) { final List<GridSampleDimension> sampleDimensions = format.sampleDimensions; if (sampleDimensions != null && sampleDimensions.size() > band) { final GridSampleDimension sd = sampleDimensions.get(band).geophysics(true); /* * We have found a sample dimension. Converts the requested range to * units of that sample dimension. If the conversion fails, we will * search for an other sample dimension. */ final NumberRange<?> convertedRange; if (range == null) { convertedRange = sd.getRange(); } else try { convertedRange = range.convertTo(sd.getUnits()); } catch (IncommensurableException e) { if (conversionError == null) { conversionError = e; } continue; } /* * Search a category for the range. If more than one category * is found, keep the best match for the requested range. */ if (convertedRange != null) { final double min = convertedRange.getMinDouble(); final double max = convertedRange.getMaxDouble(); final double center = 0.5 * (min + max); if (!Double.isNaN(center)) { final Category candidate = sd.getCategory(center); if (candidate != null) { final NumberRange<?> r = candidate.getRange(); final double cmin = r.getMinDouble(); final double cmax = r.getMaxDouble(); final double fit = (Math.min(cmax, max) - Math.max(cmin, min)) - // Intersection area (Math.max(cmax, max) - Math.min(cmin, min) - (max - min)); // Area outside requested range. if (category == null || fit > categoryFit) { category = candidate; categoryFit = fit; } } } } } } } /* * If we found a category, now create the color ramp. We use reflection in order to avoid * a direct dependency of the geotk-coverage-sql module toward the geotk-display module. */ if (category != null) try { return (RenderedImage) Class.forName("org.geotoolkit.internal.image.ColorRamp") .getMethod("paint", MeasurementRange.class, Color[].class, MathTransform1D.class, Locale.class, Map.class) .invoke(null, range, category.getColors(), category.getSampleToGeophysics(), getLocale(), properties); } catch (ClassNotFoundException exception) { throw new UnsupportedOperationException(errors().getString( Errors.Keys.MissingModule_1, "geotk-display"), exception); } catch (InvocationTargetException exception) { final Throwable cause = exception.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } if (cause instanceof Error) { throw (Error) cause; } throw new CoverageStoreException(exception); } catch (Exception exception) { // Should never happen if we didn't broke our ColorRamp helper method. throw new AssertionError(exception); } if (conversionError != null) { throw new IllegalArgumentException(errors().getString( Errors.Keys.IllegalArgument_2, "range", range), conversionError); } return null; } /** * Returns the typical pixel resolution in this layer. Values are in the unit of the * {@linkplain CoverageDatabase#getCoordinateReferenceSystem() main CRS used by the database} * (typically degrees of longitude and latitude for the horizontal part, and days for the * temporal part). Some elements of the returned array may be {@link Double#NaN NaN} if they * are unknown. */ @Override public synchronized double[] getTypicalResolution() throws CoverageStoreException { double[] resolution = this.resolution; if (resolution == null) { final SpatialDatabase database = getTableFactory(); final CoordinateSystem cs = database.spatioTemporalCRS.getCoordinateSystem(); final int xPos = AxisDirections.indexOfColinear(cs, database.horizontalCRS.getCoordinateSystem()); final int tPos = AxisDirections.indexOfColinear(cs, database.temporalCRS .getCoordinateSystem()); final int dim = cs.getDimension(); resolution = new double[dim]; Arrays.fill(resolution, Double.NaN); if (xPos >= 0) { final DomainOfLayerEntry domain; try { domain = getDomain(); } catch (SQLException e) { throw new CoverageStoreException(e); } if (domain == null) { return null; } final Dimension2D xyRes = domain.resolution; if (xyRes != null) { resolution[xPos] = xyRes.getWidth(); resolution[xPos+1] = xyRes.getHeight(); } } if (tPos >= 0) { resolution[tPos] = timeInterval; } this.resolution = resolution; } return resolution.clone(); } /** * Returns the image format used by the coverages in this layer. */ @Override public SortedSet<String> getImageFormats() throws CoverageStoreException { final FrequencySortedSet<SeriesEntry> series; try { series = getCountBySeries(); } catch (SQLException e) { throw new CoverageStoreException(e); } final int[] count = series.frequencies(); final FrequencySortedSet<String> names = new FrequencySortedSet<>(true); int i = 0; for (final SeriesEntry entry : series) { names.add(entry.format.imageFormat, count[i++]); } return names; } /** * Returns the directories where the image files are stored. */ @Override public SortedSet<File> getImageDirectories() throws CoverageStoreException { final FrequencySortedSet<SeriesEntry> series; try { series = getCountBySeries(); } catch (SQLException e) { throw new CoverageStoreException(e); } final int[] count = series.frequencies(); final FrequencySortedSet<File> directories = new FrequencySortedSet<>(true); int i = 0; for (final SeriesEntry entry : series) { if (entry.protocol.equalsIgnoreCase(SeriesEntry.FILE_PROTOCOL)) { directories.add(new File(entry.path), count[i]); } i++; } return directories; } /** * Returns the grid geometries used by the coverages in this layer, with the most frequently * used one first. The grid geometries may be 2D, 3D or 4D. The coordinate reference system * is the one declared in the {@link GridGeometryTable} for each entry. The envelope include * the vertical and temporal ranges if any. The temporal range is computed from the entries * found in the {@link GridCoverageTable}. */ @Override public synchronized SortedSet<GeneralGridGeometry> getGridGeometries() throws CoverageStoreException { SortedSet<GeneralGridGeometry> gridGeometries = this.gridGeometries; if (gridGeometries == null) { boolean hasCheckedTimeRange = false; Date startTime = null, endTime = null; final FrequencySortedSet<GridGeometryEntry> extents; try { extents = getCountByExtent(); } catch (SQLException e) { throw new CoverageStoreException(e); } if (extents == null) { return EmptySortedSet.INSTANCE; } final int[] count = extents.frequencies(); final FrequencySortedSet<GeneralGridGeometry> geometries = new FrequencySortedSet<>(true); int i = 0; for (final GridGeometryEntry entry : extents) { GeneralGridGeometry gg = entry.spatialGeometry; final DefaultTemporalCRS temporalCRS = entry.getTemporalCRS(); if (temporalCRS != null) { /* * If the geometry has a temporal component, we need to configure the grid * geometry in the time dimension ourself because the start time and end time * are layer-dependent. Fetch those start time and end time when first needed. */ if (!hasCheckedTimeRange) { hasCheckedTimeRange = true; final DateRange timeRange = getTimeRange(); if (timeRange != null) { startTime = timeRange.getMinValue(); endTime = timeRange.getMaxValue(); } } final double min = (startTime != null) ? temporalCRS.toValue(startTime) : Double.NEGATIVE_INFINITY; final double max = ( endTime != null) ? temporalCRS.toValue( endTime) : Double.POSITIVE_INFINITY; if (!Double.isInfinite(min) || !Double.isInfinite(max)) { gg = entry.geometry; final double interval; if (!Double.isNaN(timeInterval)) { final long dt = Math.round(timeInterval * GridCoverageTable.MILLIS_IN_DAY); final long epoch = temporalCRS.getDatum().getOrigin().getTime(); interval = temporalCRS.toValue(new Date(dt + epoch)); } else { interval = (max - min) / getCoverageCount(); } /* * Creates the new math transform with the same coefficients than the previous * one, except for the time dimension. The temporal "cell size" is the interval * computed above. */ final PixelInCell anchor = entry.getPixelInCell(); final MatrixSIS gridToCRS = MatrixSIS.castOrCopy(((LinearTransform) gg.getGridToCRS(anchor)).getMatrix()); final CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem(); final CoordinateSystem cs = crs.getCoordinateSystem(); final int dimension = cs.getDimension(); final int timeDimension = dimension - 1; /* * The code below makes the following assumptions, * which are checked by the assert statements below: * * 1) The temporal dimension is the last dimension. * 2) The temporal dimension is at the same index in * both the grid CRS and the "real world" CRS. */ assert AxisDirections.indexOfColinear(cs, temporalCRS.getCoordinateSystem()) == timeDimension : crs; assert gridToCRS.getElement(timeDimension, timeDimension) != 0 : gridToCRS; gridToCRS.setElement(timeDimension, timeDimension, interval); gridToCRS.setElement(timeDimension, dimension, min); GridEnvelope env = gg.getExtent(); final int[] lower = env.getLow ().getCoordinateValues(); final int[] upper = env.getHigh().getCoordinateValues(); lower[timeDimension] = 0; upper[timeDimension] = Math.max(((int) Math.round((max - min) / interval)) - 1, 0); env = new GeneralGridEnvelope(lower, upper, true); gg = new GeneralGridGeometry(env, anchor, MathTransforms.linear(gridToCRS), crs); } } geometries.add(gg, count[i++]); } gridGeometries = Collections.unmodifiableSortedSet(geometries); this.gridGeometries = gridGeometries; } return gridGeometries; } /** * Returns the geographic bounding box, or {@code null} if unknown. If the CRS used by * the database is not geographic (for example if it is a projected CRS), then this method * will transform the layer envelope from the layer CRS to a geographic CRS. */ @Override public synchronized GeographicBoundingBox getGeographicBoundingBox() throws CoverageStoreException { GeographicBoundingBox bbox = boundingBox; if (bbox == null) { final CoverageEnvelope envelope = getEnvelope(null, null); if (envelope == null) { return null; } final DefaultGeographicBoundingBox db = new DefaultGeographicBoundingBox(); try { db.setBounds(envelope); } catch (TransformException e) { throw new CoverageStoreException(e); } db.freeze(); boundingBox = bbox = db; } if (Double.isInfinite(bbox.getWestBoundLongitude()) && Double.isInfinite(bbox.getEastBoundLongitude()) && Double.isInfinite(bbox.getSouthBoundLatitude()) && Double.isInfinite(bbox.getNorthBoundLatitude())) { return null; } return bbox; } /** * Returns the envelope of this layer, optionally centered at the given date and * elevation. Callers are free to modify the returned instance before to pass it * to the {@code getCoverageReference} methods. */ @Override public CoverageEnvelope getEnvelope(final Date time, final Number elevation) throws CoverageStoreException { CoverageEnvelope envelope = coverageEnvelope; if (envelope == null) { synchronized (this) { envelope = coverageEnvelope; if (envelope == null) try { final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.envelope.clear(); table.setLayerEntry(this); table.trimEnvelope(); envelope = table.envelope.clone(); pool.release(table); coverageEnvelope = envelope; } catch (SQLException e) { throw new CoverageStoreException(e); } } } envelope = envelope.clone(); /* * Now apply the optional user parameters. */ if (time != null) { long delay = Math.round(timeInterval * (GridCoverageTable.MILLIS_IN_DAY / 2)); if (delay <= 0) { delay = GridCoverageTable.MILLIS_IN_DAY / 2; } final long t = time.getTime(); envelope.setTimeRange(new Date(t - delay), new Date(t + delay)); } if (elevation != null) { final double zmin = elevation.doubleValue(); final double zmax = zmin; // TODO: choose a better range. envelope.setVerticalRange(zmin, zmax); } return envelope; } /** * Returns a reference to every coverages available in this layer which intersect the * given envelope. * <p> * <b>Implementation note:</b> this method casts {@code Set<GridCoverageEntry>} to * {@code Set<GridCoverageReference>}. This is okay if the {@code Set} is a generic * implementation like {@link java.util.LinkedHashSet} and this class does not keep * any reference to the returned set (so no {@code Set<GridCoverageEntry>} view * exist anymore). */ @Override @SuppressWarnings({"unchecked","rawtypes"}) public Set<GridCoverageReference> getCoverageReferences(final CoverageEnvelope envelope) throws CoverageStoreException { final Set<GridCoverageEntry> entries; try { final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.setLayerEntry(this); table.envelope.setAll(envelope); entries = table.getEntries(); pool.release(table); } catch (SQLException exception) { throw new CoverageStoreException(exception); } catch (TransformException exception) { throw new MismatchedReferenceSystemException(errors() .getString(Errors.Keys.IllegalCoordinateReferenceSystem, exception)); } return (Set) entries; // See implementation note above. } /** * Returns a reference to a coverage that intersect the given envelope. If more than one * coverage intersect the given envelope, then this method will select the one which seem * the most representative. */ @Override public final GridCoverageEntry getCoverageReference(final CoverageEnvelope envelope) throws CoverageStoreException { final GridCoverageEntry entry; try { final TablePool<GridCoverageTable> pool = getTableFactory().coverages; final GridCoverageTable table = pool.acquire(); table.setLayerEntry(this); table.envelope.setAll(envelope); entry = table.envelope.isAllNaN() ? null : table.getEntry(); pool.release(table); } catch (SQLException exception) { throw new CoverageStoreException(exception); } catch (TransformException exception) { throw new MismatchedReferenceSystemException(errors() .getString(Errors.Keys.IllegalCoordinateReferenceSystem, exception)); } return entry; } /** * Adds new coverage references in the database. */ @Override public void addCoverageReferences(final Collection<?> files, final CoverageDatabaseController controller) throws CoverageStoreException { try { final WritableGridCoverageTable table = getTableFactory().getTable(WritableGridCoverageTable.class); table.setLayerEntry(this); table.addEntries(files, getCoverageDatabase(), controller); table.release(); } catch (SQLException | IOException | FactoryException exception) { throw new CoverageStoreException(exception); } } /** * Compares this layer with the specified object for equality. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (super.equals(object)) { final LayerEntry that = (LayerEntry) object; return Utilities.equals(this.timeInterval, that.timeInterval); /* * Do not test costly fields like 'fallback'. */ } return false; } /** * Invoked before serialization in order to ensure that the elements for which * the computation was deferred are now computed. */ private void writeObject(final ObjectOutputStream out) throws IOException { try { getDomain(); getSeriesMap(); getFallback(); getCountBySeries(); getCountByExtent(); getAvailableTimes(); getEnvelope(null, null); getTypicalResolution(); } catch (SQLException | CoverageStoreException e) { final InvalidObjectException ex = new InvalidObjectException(e.getLocalizedMessage()); ex.initCause(e); throw ex; } out.defaultWriteObject(); } }