/*
* 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.awt.Color;
import java.io.IOException;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.sql.Types;
import javax.swing.ComboBoxModel;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransformFactory;
import org.apache.sis.measure.NumberRange;
import org.geotoolkit.coverage.Category;
import org.geotoolkit.internal.coverage.TransferFunction;
import org.apache.sis.referencing.operation.matrix.Matrix2;
import org.geotoolkit.image.palette.PaletteFactory;
import org.geotoolkit.internal.coverage.ColorPalette;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.internal.sql.table.Table;
import org.geotoolkit.internal.sql.table.Database;
import org.geotoolkit.internal.sql.table.QueryType;
import org.geotoolkit.internal.sql.table.LocalCache;
import org.geotoolkit.internal.sql.table.CatalogException;
import org.geotoolkit.internal.sql.table.IllegalRecordException;
import org.geotoolkit.internal.sql.table.IllegalUpdateException;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
/**
* Connection to a table of {@linkplain Category categories}. This table creates a list of
* {@link Category} objects for a given sample dimension. Categories are one of the components
* required for creating a {@link org.geotoolkit.coverage.grid.GridCoverage2D}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.15
*
* @since 3.09 (derived from Seagis)
* @module
*/
final class CategoryTable extends Table {
/**
* Maximum number of bands allowed in an image. This is an arbitrary number used
* only in order to catch bad records before we create too many objects in memory.
*/
private static final int MAXIMUM_BANDS = 1000;
/**
* A transparent color for missing data.
*/
private static final Color[] TRANSPARENT = new Color[] {
new Color(0,0,0,0)
};
/**
* The choices of available palette names. Build only when first needed and cached
* in order to avoid reloading the colors from the files for every images inserted
* in the database.
*/
private transient ComboBoxModel<ColorPalette> paletteChoices;
/**
* Creates a category table.
*
* @param database Connection to the database.
*/
public CategoryTable(final Database database) {
super(new CategoryQuery(database));
}
/**
* Creates a new instance having the same configuration than the given table.
* This is a copy constructor used for obtaining a new instance to be used
* concurrently with the original instance.
*
* @param table The table to use as a template.
*/
private CategoryTable(final CategoryTable table) {
super(table);
}
/**
* Returns a copy of this table. This is a copy constructor used for obtaining
* a new instance to be used concurrently with the original instance.
*/
@Override
protected CategoryTable clone() {
return new CategoryTable(this);
}
/**
* Returns the list of categories for the given format.
*
* @param format The name of the format for which the categories are defined.
* @return The categories for each sample dimension in the given format.
* @throws SQLException if an error occurred while reading the database.
*/
public CategoryEntry getCategories(final String format) throws SQLException {
int paletteRange = 0;
String paletteName = null;
final CategoryQuery query = (CategoryQuery) this.query;
final List<Category> categories = new ArrayList<>();
final Map<Integer,Category[]> dimensions = new HashMap<>();
MathTransformFactory mtFactory = null; // Will be fetched only if needed.
MathTransform exponential = null; // Will be fetched only if needed.
int bandOfPreviousCategory = 0;
final LocalCache lc = getLocalCache();
synchronized (lc) {
final LocalCache.Stmt ce = getStatement(lc, QueryType.LIST);
final PreparedStatement statement = ce.statement;
statement.setString(indexOf(query.byFormat), format);
final int bandIndex = indexOf(query.band );
final int nameIndex = indexOf(query.name );
final int lowerIndex = indexOf(query.lower );
final int upperIndex = indexOf(query.upper );
final int c0Index = indexOf(query.c0 );
final int c1Index = indexOf(query.c1 );
final int functionIndex = indexOf(query.function);
final int colorsIndex = indexOf(query.colors );
try (ResultSet results = statement.executeQuery()) {
PaletteFactory palettes = null;
while (results.next()) {
boolean isQuantifiable = true;
final int band = results.getInt (bandIndex);
final String name = results.getString(nameIndex);
final int lower = results.getInt (lowerIndex);
final int upper = results.getInt (upperIndex);
final double c0 = results.getDouble(c0Index); isQuantifiable &= !results.wasNull();
final double c1 = results.getDouble(c1Index); isQuantifiable &= !results.wasNull();
final String function = results.getString(functionIndex);
final String colorID = results.getString(colorsIndex);
/*
* Decode the "colors" value. This string is either the RGB numeric code starting
* with '#" (as in "#D2C8A0"), or the name of a color palette (as "rainbow").
*/
Color[] colors = null;
if (colorID != null) {
final String id = colorID.trim();
if (!id.isEmpty()) try {
if (colorID.charAt(0) == '#') {
colors = new Color[] {Color.decode(id)};
} else {
if (palettes == null) {
palettes = ((TableFactory) getDatabase()).paletteFactory;
palettes.setWarningLocale(getLocale());
}
colors = palettes.getColors(colorID);
final int range = upper - lower;
if (paletteName == null || range > paletteRange) {
paletteName = colorID;
paletteRange = range;
}
}
} catch (IOException | NumberFormatException exception) {
throw new IllegalRecordException(exception, this, results, colorsIndex, name);
}
}
/*
* Creates a category for the current record. A category can be 1) qualitive,
* 2) quantitative and linear, or 3) quantitative and logarithmic.
*/
Category category;
final NumberRange<?> range = NumberRange.create(lower, true, upper, true);
if (!isQuantifiable) {
// Qualitative category.
if (colors == null) {
colors = TRANSPARENT;
}
category = new Category(name, colors, range, (MathTransform1D) null);
} else {
// Quantitative category.
if (mtFactory == null) {
mtFactory = ((SpatialDatabase) getDatabase()).getMathTransformFactory();
}
MathTransform tr;
try {
tr = mtFactory.createAffineTransform(new Matrix2(c1, c0, 0, 1));
/*
* Check for transfer function:
*
* - NULL is considered synonymous to "linear".
*
* - "log" (not to be confused with "logarithmic") is handled as
* "exponential" for compatibility with legacy databases. It was
* interpreted as log(y)=C0+C1*x.
*
* - "logarithmic" is not yet implemented, because we don't know yet
* if the log should be computed before or after the offset and scale
* factor.
*
* NOTE: The formulas used below must be consistent with the formulas in
* MetadataHelper.getGridSampleDimensions(List<SampleDimension>).
*/
if (function != null && !function.equalsIgnoreCase("linear")) {
if (function.equalsIgnoreCase("exponential") || function.equalsIgnoreCase("log")) {
// Quantitative and logarithmic category.
if (exponential == null) {
final ParameterValueGroup param = mtFactory.getDefaultParameters("Exponential");
param.parameter("base").setValue(10d); // Must be a 'double'
exponential = mtFactory.createParameterizedTransform(param);
}
tr = mtFactory.createConcatenatedTransform(tr, exponential);
} else {
throw new IllegalRecordException(errors().getString(
Errors.Keys.UnsupportedOperation_1, function),
this, results, functionIndex, name);
}
}
} catch (FactoryException exception) {
throw new CatalogException(exception);
}
try {
category = new Category(name, colors, range, (MathTransform1D) tr);
} catch (ClassCastException exception) { // If 'tr' is not a MathTransform1D.
throw new IllegalRecordException(exception, this, results, functionIndex, format);
// 'results' is closed by the above constructor.
}
}
/*
* Add to the new category to the lists. Note that the test below for the
* maximum band count is arbitrary and exists only for spotting bad records.
*/
final int minBand = Math.max(1, bandOfPreviousCategory);
if (band < minBand || band > MAXIMUM_BANDS) {
throw new IllegalRecordException(errors().getString(Errors.Keys.ValueOutOfBounds_3,
band, minBand, MAXIMUM_BANDS), this, results, bandIndex, name);
}
// If we are beginning a new band, stores the previous
// categories in the 'dimensions' map.
if (band != bandOfPreviousCategory) {
if (!categories.isEmpty()) {
store(dimensions, bandOfPreviousCategory, categories);
categories.clear();
}
bandOfPreviousCategory = band;
}
categories.add(category);
}
}
release(lc, ce);
}
if (!categories.isEmpty()) {
store(dimensions, bandOfPreviousCategory, categories);
}
return new CategoryEntry(dimensions, paletteName);
}
/**
* Puts the categories from the given list in the given map.
*/
private static void store(final Map<Integer,Category[]> dimensions, final int band,
final List<Category> categories)
{
if (dimensions.put(band, categories.toArray(new Category[categories.size()])) != null) {
throw new AssertionError(band); // Should never happen.
}
}
/**
* Adds the given categories in the database.
*
* @param format The newly created format for which to write the sample dimensions.
* @param categories The categories to add for each band.
* @throws SQLException if an error occurred while writing to the database.
*
* @since 3.13
*/
public void addEntries(final String format, final List<List<Category>> categories) throws SQLException {
final CategoryQuery query = (CategoryQuery) this.query;
final Locale locale = getLocale();
final LocalCache lc = getLocalCache();
synchronized (lc) {
boolean success = false;
transactionBegin(lc);
try {
final LocalCache.Stmt ce = getStatement(lc, QueryType.INSERT);
final PreparedStatement statement = ce.statement;
statement.setString(indexOf(query.format), format);
final int bandIndex = indexOf(query.band );
final int nameIndex = indexOf(query.name );
final int lowerIndex = indexOf(query.lower );
final int upperIndex = indexOf(query.upper );
final int c0Index = indexOf(query.c0 );
final int c1Index = indexOf(query.c1 );
final int functionIndex = indexOf(query.function);
final int colorsIndex = indexOf(query.colors );
int bandNumber = 0;
for (final List<Category> list : categories) {
statement.setInt(bandIndex, ++bandNumber);
for (Category category : list) {
category = category.geophysics(false);
final TransferFunction tf = new TransferFunction(category, locale);
if (tf.warning != null) {
throw new IllegalUpdateException(tf.warning);
}
statement.setString(nameIndex, String.valueOf(category.getName()));
statement.setInt(lowerIndex, tf.minimum);
statement.setInt(upperIndex, tf.maximum);
if (tf.isQuantitative) {
statement.setDouble(c0Index, tf.getOffset());
statement.setDouble(c1Index, tf.getScale());
if (tf.getType() != null) {
statement.setString(functionIndex, org.apache.sis.util.iso.Types.getCodeName(tf.getType()));
} else {
statement.setNull(functionIndex, Types.VARCHAR);
}
} else {
statement.setNull(c0Index, Types.DOUBLE);
statement.setNull(c1Index, Types.DOUBLE);
statement.setNull(functionIndex, Types.VARCHAR);
}
final String paletteName = getPaletteName(category.getColors());
if (paletteName != null) {
statement.setString(colorsIndex, paletteName);
} else {
statement.setNull(colorsIndex, Types.VARCHAR);
}
final int count = statement.executeUpdate();
if (count != 1) {
throw new IllegalUpdateException(locale, count);
}
}
}
release(lc, ce);
success = true;
} finally {
transactionEnd(lc, success);
}
}
}
/**
* Returns the name of the color palette for the given colors, or {@code null} if none.
* This method is invoked only during the insertion of new entries.
*/
private String getPaletteName(final Color... colors) {
if (colors != null && colors.length != 0) {
final PaletteFactory paletteFactory = ((TableFactory) getDatabase()).paletteFactory;
if (paletteChoices == null) {
paletteChoices = ColorPalette.getChoices(paletteFactory);
}
return ColorPalette.findName(colors, paletteChoices, paletteFactory);
}
return null;
}
}