/* * 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.List; import java.util.Objects; import java.sql.Array; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.opengis.referencing.operation.MathTransform1D; import org.apache.sis.util.CharSequences; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.Numbers; import org.geotoolkit.resources.Errors; import org.geotoolkit.coverage.Category; import org.geotoolkit.coverage.grid.ViewType; import org.geotoolkit.coverage.GridSampleDimension; 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.SingletonTable; import org.geotoolkit.internal.sql.table.CatalogException; import org.geotoolkit.internal.sql.table.IllegalRecordException; /** * Connection to the table of image {@linkplain Format formats}. * <p> * <b>NOTE:</b> The inherited {@link #getEntries()} method returns only the * entries using one of the formats listed to {@link #setImageFormats(String[]). * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.15 * * @since 3.09 (derived from Seagis) * @module */ final class FormatTable extends SingletonTable<FormatEntry> { /** * The sample dimensions table, created when first needed. */ private transient SampleDimensionTable sampleDimensions; /** * The last value given to {@link #setImageFormats(String[]). * Used in order to find similar formats with {@link #getEntries()}. */ private String[] imageFormats; /** * Creates a format table. * * @param database Connection to the database. */ public FormatTable(final Database database) { this(new FormatQuery(database)); } /** * Constructs a new {@code FormatTable} from the specified query. */ private FormatTable(final FormatQuery query) { super(query, query.byName); } /** * 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 FormatTable(final FormatTable 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 FormatTable clone() { return new FormatTable(this); } /** * Sets the image formats for the entries to be returned by {@link #getEntries()}. * The image formats array is typically provided by {@link FormatEntry#getImageFormats()}. * * @param formats The image formats. This method does not clone the provided array; * do not modify! */ public void setImageFormats(final String... formats) { imageFormats = formats; fireStateChanged("imageFormats"); } /** * Returns the {@link SampleDimensionTable} instance, creating it if needed. */ private SampleDimensionTable getSampleDimensionTable() throws CatalogException { SampleDimensionTable table = sampleDimensions; if (table == null) { sampleDimensions = table = getDatabase().getTable(SampleDimensionTable.class); } return table; } /** * Creates a format from the current row in the specified result set. * * @param lc The {@link #getLocalCache()} value. * @param results The result set to read. * @param identifier The identifier of the format to create. * @return The entry for current row in the specified result set. * @throws SQLException if an error occurred while reading the database. */ @Override protected FormatEntry createEntry(final LocalCache lc, final ResultSet results, final Comparable<?> identifier) throws SQLException { final FormatQuery query = (FormatQuery) super.query; final int encodingIndex = indexOf(query.packMode); final String format = results.getString(indexOf(query.plugin)); final String encoding = results.getString(encodingIndex); final String comments = results.getString(indexOf(query.comments)); final ViewType viewType; final String type = String.valueOf(encoding).toLowerCase(); switch (type) { case "photographic": viewType = ViewType.PHOTOGRAPHIC; break; case "geophysics": viewType = ViewType.GEOPHYSICS; break; case "native": viewType = ViewType.NATIVE; break; case "packed": viewType = ViewType.PACKED; break; default: { // Following constructor will close the ResultSet. throw new IllegalRecordException(errors().getString( Errors.Keys.UnknownParameter_1, encoding), this, results, encodingIndex, identifier); } } final CategoryEntry entry = getSampleDimensionTable().getSampleDimensions(identifier.toString()); GridSampleDimension[] sampleDimensions = null; String paletteName = null; if (entry != null) { sampleDimensions = entry.sampleDimensions; paletteName = entry.paletteName; } return new FormatEntry((String) identifier, format, paletteName, sampleDimensions, viewType, comments); } /** * Custom configuration of a statement which is about to be executed. In the particular case * where the query type is {@code LIST}, this method configure the statement in order to search * for formats using the same image plugin. */ @Override protected void configure(final LocalCache lc, final QueryType type, final PreparedStatement statement) throws SQLException { super.configure(lc, type, statement); switch (type) { case LIST: { if (imageFormats == null) { imageFormats = CharSequences.EMPTY_ARRAY; } final Array array = statement.getConnection().createArrayOf("varchar", imageFormats); final FormatQuery query = (FormatQuery) super.query; statement.setArray(indexOf(query.byPlugin), array); //array.free(); TODO: Revisit after we upgrated the PostgreSQL driver. break; } } } /** * Returns the size of the given list, or 0 if null. */ private static int size(final List<?> list) { return (list != null) ? list.size() : 0; } /** * Gets the range of sample values from the given category, with inclusive * bounds for consistency with the database definition. */ private static NumberRange<?> getRange(final Category category) { NumberRange<?> range = category.geophysics(false).getRange(); final Class<?> type = range.getElementType(); if (Numbers.isInteger(type)) { if (!range.isMaxIncluded() || !range.isMinIncluded() || type != Integer.class) { range = new NumberRange<>(Integer.class, (int) Math.floor(range.getMinDouble(true)), true, (int) Math.ceil (range.getMaxDouble(true)), true); } } return range; } /** * If a format exists for the given codec and sample dimensions, return it. * Otherwise returns {@code null}. * <p> * This method ignores mismatches in the following properties, because they * do not affect the numerical values computed by the transfer function: * <p> * <ul> * <li>Sample dimension names.</li> * <li>Category names.</li> * <li>Color palette (ignored because often encoded in the image format, * in which case {@link #createEntry()} will ignore it anyway).</li> * </ul> * * @param codecName The name of the Image I/O plugin. * @param bands The sample dimensions to look for. * @return An existing format, or {@code null} if none. * @throws SQLException If an error occurred while querying the database. * * @since 3.13 */ public FormatEntry find(final String codecName, final List<GridSampleDimension> bands) throws SQLException { final int numBands = size(bands); setImageFormats(FormatEntry.getImageFormats(codecName)); next: for (final FormatEntry candidate : getEntries()) { final List<GridSampleDimension> current = candidate.sampleDimensions; if (size(current) != numBands) { // Number of band don't match: look for an other format. continue next; } for (int i=0; i<numBands; i++) { final GridSampleDimension band1 = bands.get(i); final GridSampleDimension band2 = current.get(i); if (!Objects.equals(band1.getUnits(), band2.getUnits())) { // Units don't match for at least one band: look for an other format. continue next; } final List<Category> categories1 = band1.getCategories(); final List<Category> categories2 = band2.getCategories(); final int numCategories = size(categories1); if (size(categories2) != numCategories) { // Number of category don't match in at least one band: look for an other format. continue next; } for (int j=0; j<numCategories; j++) { Category category1 = categories1.get(j); Category category2 = categories2.get(j); /* * Converts the two categories to non-geophysics categories. * If we detect in this process that one category is geophysics * while the other is not, consider that we don't have a match. */ if ((category1 == (category1 = category1.geophysics(false))) != (category2 == (category2 = category2.geophysics(false)))) { continue next; } /* * Compares the sample value range (not the geophysics one) because * the former is definitive in the database. However do not convert * to geophysics categories when comparing the transforms, because * we want to differentiate "geophysics" views from the packed ones * (the former have identity transforms). */ if (!Objects.equals(getRange(category1), getRange(category2)) || !Objects.equals(category1.getSampleToGeophysics(), category2.getSampleToGeophysics())) { continue next; } } } return candidate; } return null; } /** * Creates a new format for the given sample dimensions, or returns an existing one. * If a format for the given name already exists, then this method does nothing; it * does not check the image format and the bands. Otherwise a new format created with * the given image format and the bands. * * @param name The name of the new format, or {@code null} for a default one. * @param imageFormat The name of the Image I/O plugin. * @param bands The sample dimensions to add to the database. * @return The format name. * @throws SQLException if an error occurred while writing to the database. * * @since 3.13 */ public String findOrCreate(String name, final String imageFormat, final List<GridSampleDimension> bands) throws SQLException { /* * Determine whatever the given bands are for geophysics or packed data. * If at least one band is not geophysics, we consider all of them as packed. */ ViewType type = ViewType.PHOTOGRAPHIC; check: for (final GridSampleDimension band : bands) { final List<Category> categories = band.getCategories(); if (categories != null) { for (final Category category : categories) { final MathTransform1D tr = category.getSampleToGeophysics(); if (tr != null) { if (tr.isIdentity()) { type = ViewType.GEOPHYSICS; } else { type = ViewType.PACKED; break check; } } } } } /* * Now process to the insertion in the database. Before doing the actual * insertion, we will check for existing entries inside the write lock. */ final FormatQuery query = (FormatQuery) super.query; final LocalCache lc = getLocalCache(); synchronized (lc) { boolean success = false; transactionBegin(lc); try { /* * Checks for existing entries. */ if (name == null) { final FormatEntry candidate = find(imageFormat, bands); if (candidate != null && candidate.viewType == type) { return candidate.getIdentifier(); } name = searchFreeIdentifier(lc, imageFormat); } else if (exists(name)) { return name; } /* * No existing entry fit. Adds the new entry. */ final LocalCache.Stmt ce = getStatement(lc, QueryType.INSERT); final PreparedStatement statement = ce.statement; statement.setString(indexOf(query.name), name); statement.setString(indexOf(query.plugin), imageFormat); statement.setString(indexOf(query.packMode), type.name().toLowerCase(Locale.ENGLISH)); final boolean inserted = updateSingleton(statement); release(lc, ce); if (inserted) { if (!bands.isEmpty()) { getSampleDimensionTable().addEntries(name, bands); } success = true; } } finally { transactionEnd(lc, success); } } return name; } /** * Searches for a format name not already in use. If the given string is not in use, then * it is returned as-is. Otherwise this method appends a unused decimal number to the * specified name. * * @since 3.15 */ public String searchFreeIdentifier(final String base) throws SQLException { final LocalCache lc = getLocalCache(); synchronized (lc) { return searchFreeIdentifier(lc, base); } } }