/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-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.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import javax.imageio.ImageReader;
import javax.imageio.IIOException;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.metadata.IIOMetadata;
import org.geotoolkit.nio.IOUtilities;
import org.opengis.util.FactoryException;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.metadata.content.TransferFunctionType;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.apache.sis.measure.Range;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Localized;
import org.geotoolkit.util.DateRange;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.Classes;
import org.geotoolkit.image.io.mosaic.Tile;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.image.io.metadata.SampleDimension;
import org.geotoolkit.image.io.ImageReaderAdapter;
import org.geotoolkit.image.io.XImageIO;
import org.geotoolkit.internal.image.io.Formats;
import org.geotoolkit.internal.image.io.DimensionAccessor;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
import org.geotoolkit.internal.sql.table.NoSuchRecordException;
import org.geotoolkit.internal.coverage.TransferFunction;
import org.geotoolkit.metadata.Citations;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.crs.DefaultTemporalCRS;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.geotoolkit.referencing.cs.DiscreteCoordinateSystemAxis;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.ViewType;
import org.geotoolkit.coverage.Category;
import org.geotoolkit.resources.Errors;
import static org.geotoolkit.internal.image.io.DimensionAccessor.fixRoundingError;
/**
* A structure which contain the information to be added in the {@linkplain CoverageDatabase
* Coverage Database} for a new coverage reference. The information provided in this class
* match closely the layout of the coverage database.
* <p>
* Instances of this class are created by {@link Layer#addCoverageReferences(Collection,
* CoverageDatabaseController)} and dispatched to {@link CoverageDatabaseListener}s. The
* listeners can modify the field values {@linkplain CoverageDatabaseEvent#isBefore() before}
* the insertion in the database occurs.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.21
*
* @see CoverageDatabaseListener
*
* @since 3.12 (derived from Seagis)
* @module
*/
public final class NewGridCoverageReference {
/**
* The authorities of {@link #horizontalSRID} and {@link #verticalSRID} codes, in preference
* order. The SRID should be the primary keys in the {@code "spatial_ref_sys"} table. If we
* failed to determine the primary key, we will rely on the observation that the primary key
* values are often the EPSG codes (but not necessarily). We shall declare here only the
* authority which are known to use numerical codes.
*/
private static final Citation[] AUTHORITIES = {
Citations.POSTGIS,
Citations.EPSG
};
/**
* The range of sample values to use if no transfer function is defined.
* Note that the value 0 is reserved for "no data".
*/
private static final NumberRange<Integer> PACKED_RANGE = NumberRange.create(1, true, 255, true);
/**
* The originating database.
*/
private final SpatialDatabase database;
/**
* The path to the coverage file (not including the filename), or {@code null} if the filename
* has no parent directory. The full path to the input file is
* "{@linkplain #path}/{@linkplain #filename}.{@linkplain #extension}".
*
* @see #filename
* @see #extension
* @see #getFile()
*/
public final Path path;
/**
* The filename, not including the {@linkplain #path} and {@linkplain #extension}.
*
* @see #path
* @see #extension
* @see #getFile()
*/
public final String filename;
/**
* The filename extension (not including the leading dot), or {@code null} if none.
*
* @see #path
* @see #filename
* @see #getFile()
*/
public final String extension;
/**
* The zero-based index of the image to be inserted in the database. If there is many
* images to insert for many different {@linkplain #dateRanges date ranges}, then this
* is the index of the first image, i.e.:
* <p>
* <ul>
* <li>The temporal extent of the image at index {@code imageIndex} is
* <code>{@linkplain #dateRanges}[0]</code>.</li>
* <li>The temporal extent of the image at index {@code imageIndex + 1} is
* <code>{@linkplain #dateRanges}[1]</code>.</li>
* <li><i>etc.</i></li>
* <li>Finally the temporal extent of the image at index {@code imageIndex + n}
* is <code>{@linkplain #dateRanges}[n]</code> where <var>n</var> =
* {@code dateRanges.length - 1}.</li>
* </ul>
*
* @since 3.16
*/
public int imageIndex;
/**
* The name of the coverage format. It shall be one of the primary key values in the
* {@code "Formats"} table. Note that this is not necessarily the same name than the
* {@linkplain ImageReaderSpi#getFormatNames() image format name}.
* <p>
* This field is initialized to the format which seems the best fit. A list of
* alternative formats can be obtained by {@link #getAlternativeFormats()}.
*
* @see #isFormatDefined()
* @see #getAlternativeFormats()
* @see #refresh()
*/
public String format;
/**
* The sample dimensions for coverages associated with the {@linkplain #format}, or an empty
* list if undefined. If non-empty, then the list size is equals to the number of bands.
* <p>
* Each {@code GridSampleDimension} specifies how to convert pixel values to geophysics values,
* or conversely. Their type (geophysics or not) is format dependent. For example coverages
* read from PNG files will typically store their data as integer values (non-geophysics),
* while coverages read from ASCII files will often store their pixel values as real numbers
* (geophysics values).
* <p>
* The content of this list can be modified in-place.
*
* @see #refresh()
*
* @since 3.13
*/
public final List<GridSampleDimension> sampleDimensions;
/**
* The format entry which seems the best fit. The {@link #format} field is initialized
* to the name of this format. The most interesting information from this field is the
* list of sample dimensions.
*/
final FormatEntry bestFormat;
/**
* Some formats which may be applicable as an alternative to {@code series.format}.
* This list is created by {@link #getAlternativeFormats()} when first needed. The
* content shall not be modified after creation.
*/
private FormatEntry[] alternativeFormats;
/**
* The image reader provider.
*/
private final ImageReaderSpi spi;
/**
* The image bounds. The rectangle {@linkplain Rectangle#width width} and
* {@linkplain Rectangle#height height} must be set to the image size. The
* ({@linkplain Rectangle#x x},{@linkplain Rectangle#y y}) origin is usually (0,0),
* but different value are allowed. For example the origin can be set to
* the {@linkplain Tile#getLocation location of a tile} in tiled images.
* <p>
* If the (x,y) origin is different than (0,0), then it will be interpreted as the
* translation to apply on the grid <em>before</em> to apply the {@link #gridToCRS}
* transform at reading time.
* <p>
* This field is never {@code null}. However users can modify it before the
* new entry is inserted in the database.
*/
public final Rectangle imageBounds;
/**
* The <cite>grid to CRS</cite> transform, which maps always the pixel
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner. This transform
* does <em>not</em> include the (x,y) translation of the {@link #imageBounds}.
* <p>
* This field is never {@code null}. However users can modify it before the
* new entry is inserted in the database.
*/
public final AffineTransform gridToCRS;
/**
* The horizontal CRS identifier. This shall be the value of a primary key
* in the {@code "spatial_ref_sys"} PostGIS table. The value may be 0 if this
* class found no information about the horizontal SRID.
*/
public int horizontalSRID;
/**
* The vertical CRS identifier, ignored if {@link #verticalValues} is {@code null}.
* When not ignored, this shall be the value of a primary key in the
* {@code "spatial_ref_sys"} PostGIS table. The value may be 0 if this
* class found no information about the vertical SRID.
*/
public int verticalSRID;
/**
* The vertical coordinate values, or {@code null} if none.
*/
public double[] verticalValues;
/**
* The date range, or {@code null} if none. This array usually contains only one element,
* but more than one time range is allowed if the image file contains data at many times.
* In the later case, the sequence of date ranges is associated to the sequence of
* {@linkplain #imageIndex image indices}, i.e.:
* <p>
* <ul>
* <li>{@code dateRanges[0]} is the temporal extent of the image at index {@link #imageIndex}.</li>
* <li>{@code dateRanges[1]} is the temporal extent of the image at index {@link #imageIndex} + 1.</li>
* <li><i>etc.</i></li>
* <li>Finally, {@code dateRanges[n]} is the temporal extent of the image at index
* {@link #imageIndex} + n where <var>n</var> = {@code dateRanges.length - 1}.</li>
* </ul>
*/
public DateRange[] dateRanges;
/**
* Creates a new instance which is a copy of the given instance except for the input file,
* image index and time range. This method is used only when iterating over the content of
* an aggregate (typically a NcML file).
* <p>
* This constructor does not clone the references to mutable objects.
* Consequently this instance is not allowed to be made visible through public API.
*
* {@section Note for implementors}
* The {@link WritableGridCoverageTable#addEntries} method assumes that the instance created by
* this method uses the same format and the same spatial extent than the master entry. If this
* assumption doesn't hold anymore in a future version, then {@code WritableGridCoverageTable}
* needs to be updated (see comments in its code).
*
* @param master The reference to copy.
* @param file The path, filename and index to the new image file.
* @param dateIndex Index of the element to select in the {@code dateRanges} array.
*
* @since 3.16
* @deprecated use {@link #NewGridCoverageReference(NewGridCoverageReference, Path, int)} instead
*/
NewGridCoverageReference(final NewGridCoverageReference master, final File file, final int dateIndex) {
this(master, file.toPath(), dateIndex);
}
/**
* Creates a new instance which is a copy of the given instance except for the input file,
* image index and time range. This method is used only when iterating over the content of
* an aggregate (typically a NcML file).
* <p>
* This constructor does not clone the references to mutable objects.
* Consequently this instance is not allowed to be made visible through public API.
*
* {@section Note for implementors}
* The {@link WritableGridCoverageTable#addEntries} method assumes that the instance created by
* this method uses the same format and the same spatial extent than the master entry. If this
* assumption doesn't hold anymore in a future version, then {@code WritableGridCoverageTable}
* needs to be updated (see comments in its code).
*
* @param master The reference to copy.
* @param file The path, filename and index to the new image file.
* @param dateIndex Index of the element to select in the {@code dateRanges} array.
*
* @since 3.16
*/
NewGridCoverageReference(final NewGridCoverageReference master, final Path file, final int dateIndex) {
String filename = file.getFileName().toString();
String extension = null;
final int s = filename.lastIndexOf('.');
if (s > 0) {
extension = filename.substring(s+1);
filename = filename.substring(0, s);
}
this.database = master.database;
this.path = file.getParent();
this.filename = filename;
this.extension = extension;
this.format = master.format;
this.sampleDimensions = master.sampleDimensions;
this.bestFormat = master.bestFormat;
this.alternativeFormats = master.alternativeFormats;
this.spi = master.spi;
this.imageBounds = master.imageBounds;
this.gridToCRS = master.gridToCRS;
this.horizontalSRID = master.horizontalSRID;
this.verticalSRID = master.verticalSRID;
this.verticalValues = master.verticalValues;
this.dateRanges = new DateRange[] {master.dateRanges[dateIndex]};
// 'imageIndex' needs to be left to 0.
}
/**
* Creates an entry for the given tile. This constructor does <strong>not</strong> read the
* image file, since we usually don't want to parse the metadata for every tiles (usually,
* every tiles share the same metadata). Consequently, caller may need to set the metadata
* explicitly using their own {@link CoverageDatabaseController} instance.
*
* @param database The database where the new entry will be added.
* @param tile The tile to use for the entry.
* @throws IOException If an error occurred while fetching some tile properties.
*/
NewGridCoverageReference(final SpatialDatabase database, final Tile tile)
throws SQLException, IOException, FactoryException
{
this(database, null,
tile.getInput(),
tile.getImageIndex(),
tile.getImageReaderSpi(),
tile.getRegion(),
tile.getGridToCRS(),
(tile instanceof NewGridCoverage) ? ((NewGridCoverage) tile).crs : null,
null);
}
/**
* Creates an entry for the given reader. The {@linkplain ImageReader#setInput(Object)
* reader input must be set} by the caller before to invoke this constructor.
*
* @param database The database where the new entry will be added.
* @param reader The image reader with its input set.
* @param input The original input. May not be the same than {@link ImageReader#getInput()}
* because the later may have been transformed in an image input stream.
* @param imageIndex Index of the image to read.
* @param disposeReader {@code true} if {@link ImageReader#dispose()} should be invoked on
* the given {@code reader} after this method finished its work.
* @throws IOException If an error occurred while reading the image.
*/
NewGridCoverageReference(final SpatialDatabase database, final ImageReader reader,
final Object input, final int imageIndex, final boolean disposeReader)
throws SQLException, IOException, FactoryException
{
this(database, reader, input, imageIndex, reader.getOriginatingProvider(),
new Rectangle(reader.getWidth(imageIndex), reader.getHeight(imageIndex)), null, null,
getSpatialMetadata(reader, imageIndex));
/*
* Close the reader but do not dispose it (unless we were asked to),
* since it may be used for the next entry.
*/
XImageIO.close(reader);
if (disposeReader) {
reader.dispose();
}
}
/**
* Creates an entry for the given tile or reader.
*
* @param database The database where the new entry will be added.
* @param reader The image reader with its input set, or {@code null} if none.
* @param input The original input (<strong>not</strong> the input stream).
* @param imageIndex Index of the image to read.
* @param spi The provider of the {@code reader}, or {@link Tile#getImageReaderSpi()}.
* @param imageBounds The image size (in pixels) and its location (in the case of tiles only).
* @param gridToCRS The transform to real world, or {@code null} for fetching from metadata.
* @param crs The coordinate reference system, or {@code null} for fetching from metadata.
* @param metadata The metadata, or {@code null} if none,
* @throws IOException If an error occurred while reading the image.
*/
private NewGridCoverageReference(final SpatialDatabase database,
final ImageReader reader, Object input, final int imageIndex, final ImageReaderSpi spi,
final Rectangle imageBounds, AffineTransform gridToCRS, CoordinateReferenceSystem crs,
SpatialMetadata metadata) throws SQLException, IOException, FactoryException
{
this.database = database;
this.imageIndex = imageIndex;
this.spi = spi;
this.imageBounds = imageBounds;
/*
* Get the input, which must be an instance of File.
* Split that input file into the path components.
*/
input = IOUtilities.tryToPath(input);
if (!(input instanceof Path)) {
throw new IIOException(Errors.format(Errors.Keys.IllegalClass_2,
Classes.getShortClassName(input), File.class));
}
final Path inputFile = (Path) input;
path = inputFile.getParent();
final String name = inputFile.getFileName().toString();
final int split = name.lastIndexOf('.');
if (split >= 0) {
filename = name.substring(0, split);
extension = name.substring(split + 1);
} else {
filename = name;
extension = null;
}
/*
* Get the metadata. We usually need the image metadata. However some formats may
* store their information only in stream metadata, so we will fallback on stream
* metadata if there is no image metadata.
*/
String imageFormat = Formats.getDisplayName(ImageReaderAdapter.Spi.unwrap(spi));
if (imageFormat == null) {
imageFormat = IOUtilities.extension(inputFile);
}
if (imageFormat.isEmpty()) {
throw new IOException(Errors.format(Errors.Keys.UndefinedFormat));
}
final MetadataHelper helper = (metadata != null) ? new MetadataHelper(
(reader instanceof Localized) ? (Localized) reader : null) : null;
/*
* Get the geolocalization from the image, if it was not provided by a Tile instance.
*/
if (gridToCRS != null) {
// Tile.getGridToCRS() returns an immutable AffineTransform.
// We want to allow modifications.
gridToCRS = new AffineTransform(gridToCRS);
} else if (metadata != null) {
gridToCRS = helper.getAffineTransform(metadata.getInstanceForType(RectifiedGrid.class), null);
} else {
gridToCRS = new AffineTransform();
}
this.gridToCRS = gridToCRS;
/*
* Get the CRS, then try to infer the horizontal and vertical SRID from it.
* This code scan the "spatial_ref_sys" PostGIS table until matches are found,
* or leaves the SRID to 0 if no match is found.
*/
if (crs == null && metadata != null) {
crs = metadata.getInstanceForType(CoordinateReferenceSystem.class);
}
if (crs != null) {
/*
* Horizontal CRS.
*/
final CRSAuthorityFactory crsFactory = database.getCRSAuthorityFactory();
final CoordinateReferenceSystem horizontalCRS = CRS.getHorizontalComponent(crs);
if (horizontalCRS != null) {
final Integer id = getIdentifier(horizontalCRS, crsFactory);
if (id != null) {
horizontalSRID = id;
}
}
/*
* Vertical CRS. Extract also the vertical ordinates, if any.
*/
final VerticalCRS verticalCRS = CRS.getVerticalComponent(crs, true);
if (verticalCRS != null) {
final Integer id = getIdentifier(verticalCRS, crsFactory);
if (id != null) {
verticalSRID = id;
}
final CoordinateSystemAxis axis = verticalCRS.getCoordinateSystem().getAxis(0);
if (axis instanceof DiscreteCoordinateSystemAxis<?>) {
final DiscreteCoordinateSystemAxis<?> da = (DiscreteCoordinateSystemAxis<?>) axis;
final int length = da.length();
for (int i=0; i<length; i++) {
final Comparable<?> value = da.getOrdinateAt(i);
if (value instanceof Number) {
if (verticalValues == null) {
verticalValues = new double[length];
Arrays.fill(verticalValues, Double.NaN);
}
verticalValues[i] = ((Number) value).doubleValue();
}
}
}
}
/*
* Temporal CRS. Extract also the time ordinate range, if any.
*/
final TemporalCRS temporalCRS = CRS.getTemporalComponent(crs);
if (temporalCRS != null) {
final CoordinateSystemAxis axis = temporalCRS.getCoordinateSystem().getAxis(0);
if (axis instanceof DiscreteCoordinateSystemAxis<?>) {
final DiscreteCoordinateSystemAxis<?> da = (DiscreteCoordinateSystemAxis<?>) axis;
DefaultTemporalCRS c = null; // To be created when first needed.
dateRanges = new DateRange[da.length()];
for (int i=0; i<dateRanges.length; i++) {
Range<?> r = da.getOrdinateRangeAt(i);
if (!(r instanceof DateRange)) {
if (c == null) {
c = DefaultTemporalCRS.castOrCopy(temporalCRS);
}
r = new DateRange(
c.toDate(((Number) r.getMinValue()).doubleValue()), r.isMinIncluded(),
c.toDate(((Number) r.getMaxValue()).doubleValue()), r.isMaxIncluded());
}
dateRanges[i] = (DateRange) r;
}
} else {
final DefaultTemporalCRS c = DefaultTemporalCRS.castOrCopy(temporalCRS);
dateRanges = new DateRange[] {
new DateRange(c.toDate(axis.getMinimumValue()), c.toDate(axis.getMaximumValue()))
};
}
}
}
/*
* Get the sample dimensions. This code extracts the SampleDimensions from the metadata,
* converts them to GridSampleDimensions, then checks if the resulting sample dimensions
* are native, packed or geophysics.
*/
ViewType packMode = ViewType.NATIVE;
List<GridSampleDimension> sampleDimensions = null;
if (metadata != null) {
final DimensionAccessor dimHelper = new DimensionAccessor(metadata);
if (reader != null && dimHelper.isScanSuggested(reader, imageIndex)) {
dimHelper.scanValidSampleValue(reader, imageIndex);
}
sampleDimensions = helper.getGridSampleDimensions(metadata.getListForType(SampleDimension.class));
if (sampleDimensions != null) {
/*
* Replaces geophysics sample dimensions by new sample dimensions using the
* [1...255] range of packed values, and 0 for "no data". We don't do that
* in the default MetadataHelper implementation because the chosen range is
* arbitrary. However this is okay to make such arbitrary choice in the particular
* case of NewGridCoverageReference, because the chosen range will be saved
* in the database (the user can also modify the range).
*/
final GridSampleDimension[] bands = sampleDimensions.toArray(new GridSampleDimension[sampleDimensions.size()]);
for (int i=0; i<bands.length; i++) {
final GridSampleDimension band = bands[i].geophysics(false);
final List<Category> categories = band.getCategories();
if (categories == null) {
continue;
}
for (int j=categories.size(); --j>=0;) {
Category c = categories.get(j);
final TransferFunction tf = new TransferFunction(c, null);
if (!tf.isQuantitative) {
continue;
}
if (tf.isGeophysics) {
/*
* In the geophysics case, we are free to choose whatever upper
* value please us. We are using 255 here, the maximum allowed
* for a 8-bits indexed image.
*/
c = new Category(c.getName(), c.getColors(), PACKED_RANGE, c.getRange());
bands[i] = packSampleDimension(band, c).geophysics(true);
packMode = ViewType.GEOPHYSICS;
} else if (tf.minimum < 0 && TransferFunctionType.LINEAR.equals(tf.getType())) {
/*
* In the signed integer values case, the offset applied here must
* be consistent with the sample conversion applied by the image
* reader when SampleConversionType.SHIFT_SIGNED_INTEGERS is set.
*/
// Upper sample value: Add 1 because value 0 is reserved for
// "no data", and add 1 again because 'upper' is exclusive.
final int upper = (tf.maximum - tf.minimum) + 2;
double offset = fixRoundingError(tf.getOffset() - tf.getScale() * (1 - tf.minimum));
c = new Category(c.getName(), c.getColors(), 1, upper, tf.getScale(), offset);
bands[i] = packSampleDimension(band, c);
packMode = ViewType.PACKED;
}
/*
* MetadataHelper should have created at most one quantitative category
* (usually the last one) recognized by its non-null transfer function.
* In the uncommon case were there is more quantitative categories, it
* is the user responsibility to edit the fields (using the widget for
* instance). We stop the loop in order to avoid conflicts.
*/
break;
}
}
sampleDimensions = Arrays.asList(bands);
}
}
/*
* Search if a format already exists in the database for the sample dimensions.
* If no existing format is found, create a new FormatEntry but do not add it
* in the database yet.
*/
final FormatTable formatTable = database.getTable(FormatTable.class);
FormatEntry candidate = formatTable.find(imageFormat, sampleDimensions);
if (candidate == null) {
GridSampleDimension[] bands = null;
if (sampleDimensions != null) {
bands = new GridSampleDimension[sampleDimensions.size()];
for (int i=0; i<bands.length; i++) {
bands[i] = sampleDimensions.get(i).geophysics(false);
}
}
candidate = new FormatEntry(formatTable.searchFreeIdentifier(imageFormat),
imageFormat, null, bands, packMode, null);
}
formatTable.release();
bestFormat = candidate;
format = candidate.getIdentifier();
this.sampleDimensions = (candidate.sampleDimensions != null) ?
new ArrayList<>(candidate.sampleDimensions) :
new ArrayList<GridSampleDimension>();
}
/**
* Extracts spatial metadata from the given reader. First, this method tries to extract the
* image metadata. If they are not suitable, then this method fallback on the stream metadata.
* This method does not wraps other implementations in {@code SpatialMetadata} implementation.
* <p>
* This method is a workaround for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*
* @param reader The image reader from which to extract the metadata, or {@code null}.
* @param imageIndex The index of the image from which to extract metadata.
* @return The metadata, or {@code null} if none.
* @throws IOException If an error occurred while reading the metadata.
*/
private static SpatialMetadata getSpatialMetadata(final ImageReader reader, final int imageIndex)
throws IOException
{
SpatialMetadata metadata = null;
if (reader != null) {
IIOMetadata candidate = reader.getImageMetadata(imageIndex);
if (candidate instanceof SpatialMetadata) {
metadata = (SpatialMetadata) candidate;
} else {
candidate = reader.getStreamMetadata();
if (candidate instanceof SpatialMetadata) {
metadata = (SpatialMetadata) candidate;
}
}
}
return metadata;
}
/**
* Creates a new sample dimension defined as the "no data" category together with the
* given category. The dimension name and units are copied from the old sample dimension.
*/
private static GridSampleDimension packSampleDimension(final GridSampleDimension band, final Category category) {
return new GridSampleDimension(band.getDescription(),
new Category[] {Category.NODATA, category}, band.getUnits());
}
/**
* Returns the identifier of the given CRS. This method search first in the PostGIS
* {@code "spatial_ref_sys"} table. If no identifier is found in that table, then it
* search for the EPSG code on the assumption that PostGIS codes are often the same
* numerical values than EPSG codes (not that there is nothing enforcing that; this
* is only an observed common practice).
*
* @param crs The CRS for which the identifier is wanted.
* @param crsFactory The PostGIS CRS factory, or {@code null} if none.
* @return The identifier, or {@code null} if none.
* @throws FactoryException If an error occurred while searching for the identifier.
*/
private static Integer getIdentifier(final CoordinateReferenceSystem crs, final CRSAuthorityFactory crsFactory)
throws FactoryException
{
if (crsFactory instanceof GeodeticAuthorityFactory) {
IdentifiedObject identifiedCRS = ((GeodeticAuthorityFactory) crsFactory)
.newIdentifiedObjectFinder().findSingleton(crs);
if (identifiedCRS == null) {
identifiedCRS = crs;
}
for (final Citation authority : AUTHORITIES) {
final Identifier id = IdentifiedObjects.getIdentifier(identifiedCRS, authority);
if (id != null) {
final String code = id.getCode();
if (id != null) try {
return Integer.valueOf(code);
} catch (NumberFormatException e) {
throw new FactoryException(Errors.format(Errors.Keys.UnparsableNumber_1, id), e);
}
}
}
}
return org.geotoolkit.referencing.IdentifiedObjects.lookupEpsgCode(crs, true);
}
/**
* Returns the path to the coverage file as
* "{@linkplain #path}/{@linkplain #filename}.{@linkplain #extension}".
*
* @return The path to the image file, or {@code null} if {@link #filename} is null.
*
* @see #path
* @see #filename
* @see #extension
*/
public Path getFile() {
String name = filename;
if (name == null) {
return null;
}
if (extension != null) {
name = name + '.' + extension;
}
return path.resolve(name);
}
/**
* Returns a list of formats which may be used as an alternative to {@link #format}.
* This method can be invoked from Graphical User Interface wanting to provide a
* choice to user.
*
* @return A list of formats which may be used as an alternative to {@link #format}.
* @throws CoverageStoreException If an error occurred while fetching the list of
* alternative formats from the database.
*
* @since 3.13
*/
public String[] getAlternativeFormats() throws CoverageStoreException {
FormatEntry[] alternativeFormats = this.alternativeFormats;
if (alternativeFormats == null) {
/*
* Fetch the alternative formats from the database when first needed.
*/
final Collection<FormatEntry> formats;
try {
final FormatTable table = database.getTable(FormatTable.class);
table.setImageFormats(bestFormat.getImageFormats());
formats = table.getEntries();
table.release();
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
alternativeFormats = formats.toArray(new FormatEntry[formats.size()]);
/*
* Retain only the formats having the same number of bands.
*/
final int numBands = sampleDimensions.size();
if (numBands != 0) {
int count = 0;
for (int i=0; i<alternativeFormats.length; i++) {
final FormatEntry candidate = alternativeFormats[i];
final List<GridSampleDimension> cb = candidate.sampleDimensions;
if (cb == null || cb.size() == numBands) {
alternativeFormats[count++] = candidate;
}
}
alternativeFormats = ArraysExt.resize(alternativeFormats, count);
}
this.alternativeFormats = alternativeFormats;
}
/*
* Return the format names (not the format entries, which are not public API).
*/
final String[] names = new String[alternativeFormats.length];
for (int i=0; i<names.length; i++) {
names[i] = alternativeFormats[i].identifier.toString();
}
return names;
}
/**
* Returns {@code true} if the {@linkplain #format} is already defined in the database,
* or {@code false} if this is a new format.
*
* @return {@code true} if the current format is defined in the database.
* @throws CoverageStoreException If an error occurred while reading from the database.
*
* @since 3.13
*
* @see #format
*/
public boolean isFormatDefined() throws CoverageStoreException {
final boolean isDefined;
try {
final FormatTable table = database.getTable(FormatTable.class);
isDefined = table.exists(format);
table.release();
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
return isDefined;
}
/**
* Recomputes some attributes in this {@code NewGridCoverageReference}. This method can be
* invoked after one of the following attributes changed:
* <p>
* <ul>
* <li>{@link #format}</li>
* </ul>
* <p>
* The current implementation recomputes the following attributes. Note that this list
* may be expanded in a future version.
* <p>
* <ul>
* <li>{@link #sampleDimensions}</li>
* </ul>
*
* @throws CoverageStoreException If an error occurred while reading from the database.
*
* @since 3.13
*/
public void refresh() throws CoverageStoreException {
sampleDimensions.clear();
final List<GridSampleDimension> newContent;
if (bestFormat.getIdentifier().equals(format)) {
newContent = bestFormat.sampleDimensions;
} else {
final FormatEntry entry;
try {
final FormatTable table = database.getTable(FormatTable.class);
try {
entry = table.getEntry(format);
} catch (NoSuchRecordException e) {
table.release();
return;
}
table.release();
} catch (SQLException e) {
throw new CoverageStoreException(e);
}
newContent = entry.sampleDimensions;
}
if (newContent != null) {
sampleDimensions.addAll(newContent);
}
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)).append('[');
boolean isNext = false;
fill: for (int i=0; ;i++) {
final String label;
final Object value;
switch (i) {
case 0: label="format"; value=format; break;
case 1: label="file"; value=getFile(); break;
default: break fill;
}
if (value != null) {
if (isNext) {
buffer.append(", ");
}
buffer.append(label).append('=').append(value);
isNext = true;
}
}
return buffer.append(']').toString();
}
}