/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.referencing.factory.epsg; import java.io.File; import java.io.ObjectStreamException; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogRecord; import java.awt.RenderingHints; import javax.naming.NamingException; import javax.sql.DataSource; import javax.measure.unit.NonSI; import javax.measure.unit.SI; import javax.measure.unit.Unit; import org.geotools.factory.GeoTools; import org.geotools.factory.Hints; import org.geotools.io.TableWriter; import org.geotools.measure.Units; import org.geotools.metadata.iso.citation.CitationImpl; import org.geotools.metadata.iso.citation.Citations; import org.geotools.metadata.iso.extent.ExtentImpl; import org.geotools.metadata.iso.extent.GeographicBoundingBoxImpl; import org.geotools.metadata.iso.quality.AbsoluteExternalPositionalAccuracyImpl; import org.geotools.metadata.iso.quality.QuantitativeResultImpl; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.parameter.DefaultParameterDescriptorGroup; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.cs.DefaultCoordinateSystemAxis; import org.geotools.referencing.datum.BursaWolfParameters; import org.geotools.referencing.datum.DefaultGeodeticDatum; import org.geotools.referencing.factory.AbstractCachedAuthorityFactory; import org.geotools.referencing.factory.BufferedAuthorityFactory; import org.geotools.referencing.factory.DirectAuthorityFactory; import org.geotools.referencing.factory.IdentifiedObjectFinder; import org.geotools.referencing.operation.DefaultConcatenatedOperation; import org.geotools.referencing.operation.DefaultOperation; import org.geotools.referencing.operation.DefaultOperationMethod; import org.geotools.referencing.operation.DefiningConversion; import org.geotools.resources.CRSUtilities; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.util.LocalName; import org.geotools.util.ObjectCache; import org.geotools.util.ScopedName; import org.geotools.util.SimpleInternationalString; import org.geotools.util.Version; import org.geotools.util.logging.Logging; import org.opengis.metadata.Identifier; import org.opengis.metadata.citation.Citation; import org.opengis.metadata.extent.Extent; import org.opengis.metadata.quality.EvaluationMethodType; import org.opengis.metadata.quality.PositionalAccuracy; import org.opengis.parameter.InvalidParameterValueException; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.NoSuchIdentifierException; import org.opengis.referencing.crs.CRSFactory; import org.opengis.referencing.crs.CompoundCRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeneralDerivedCRS; import org.opengis.referencing.crs.GeocentricCRS; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CSFactory; import org.opengis.referencing.cs.CartesianCS; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.cs.EllipsoidalCS; import org.opengis.referencing.cs.SphericalCS; import org.opengis.referencing.cs.VerticalCS; import org.opengis.referencing.datum.Datum; import org.opengis.referencing.datum.DatumFactory; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.EngineeringDatum; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.datum.PrimeMeridian; import org.opengis.referencing.datum.VerticalDatum; import org.opengis.referencing.datum.VerticalDatumType; import org.opengis.referencing.operation.ConcatenatedOperation; import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Projection; import org.opengis.referencing.operation.Transformation; import org.opengis.util.GenericName; import org.opengis.util.InternationalString; /** * A coordinate reference system factory backed by the EPSG database tables. * <p> * The EPSG database is freely available at <A HREF="http://www.epsg.org">http://www.epsg.org</a>. * Current version of this class requires EPSG database version 6.6 or above. * <p> * This factory makes use of a provided {@link ObjectCache}, and may be deployed in stand aline fashion; * or as a worker for a {@link MultiEpsgFactory}. * <p> * This class is abstract - please see the subclasses for dialect specific implementations: * <ul> * <li>{@link AccessDialectEpsgFactory}</li> * <li>{@link AnsiDialectEpsgFactory}</li> * <li>{@link OracleDialectEpsgFactory}</li> * </ul> * * These factories accepts names as well as numerical identifiers. For example * "<cite>NTF (Paris) / France I</cite>" and {@code "27581"} both fetchs the same object. * However, names may be ambiguous since the same name may be used for more than one object. * This is the case of "WGS 84" for example. If such an ambiguity is found, an exception * will be thrown. If names are not wanted as a legal EPSG code, subclasses can override the * {@link #isPrimaryKey} method. * * @since 2.4 * * @source $URL$ * @version $Id$ * @author Yann Cézard * @author Martin Desruisseaux (IRD) * @author Rueben Schulz * @author Matthias Basler * @author Andrea Aime */ public abstract class AbstractEpsgFactory extends AbstractCachedAuthorityFactory { /// Datum shift operation methods /** First Bursa-Wolf method. */ private static final int BURSA_WOLF_MIN_CODE = 9603; /** Last Bursa-Wolf method. */ private static final int BURSA_WOLF_MAX_CODE = 9607; /** Rotation frame method. */ private static final int ROTATION_FRAME_CODE = 9607; /** Dummy operation to ignore. */ private static final int DUMMY_OPERATION = 1; /** * The name for the transformation accuracy metadata. */ private static final InternationalString TRANSFORMATION_ACCURACY = Vocabulary.formatInternational(VocabularyKeys.TRANSFORMATION_ACCURACY); /** * The calendar instance for creating {@link java.util.Date} objects from a year * (the "epoch" in datum definition). We use the local timezone, which may not be * quite accurate. But there is no obvious timezone for "epoch", and the "epoch" * is approximative anyway. */ private final Calendar calendar = Calendar.getInstance(); // FIXME: move into factories? /** * The authority for this database. Will be created only when first needed. * This authority will contains the database version in the {@linkplain Citation#getEdition * edition} attribute, together with the {@linkplain Citation#getEditionDate edition date}. */ private transient Citation authority; //FIXME: always EPSG /** * A DataSource to the EPSG database being used. */ protected javax.sql.DataSource dataSource; /** * The connection to the EPSG database - this is create in a lazy manner from the DataSource. * <p> * This field is managed as part of our connection lifecycle. */ private Connection connection; /** * A pool of prepared statements. Key are {@link String} object related to their * originating method name (for example "Ellipsoid" for {@link #createEllipsoid}, * while values are {@link PreparedStatement} objects. * <p> * <strong>Note:</strong> It is okay to use {@link IdentityHashMap} instead of {@link HashMap} * because the keys will always be the exact same object, namely the hard-coded argument given * to calls to {@link #prepareStatement} in this class. * <p> * This field is managed as part of our connection lifecycle. */ private final Map<String,PreparedStatement> statements = new IdentityHashMap<String,PreparedStatement>(); /** * Last object type returned by {@link #createObject}, or -1 if none. * This type is an index in the {@link #TABLES_INFO} array and is * strictly for {@link #createObject} internal use. */ private int lastObjectType = -1; /** * The last table in which object name were looked for. This is for internal use * by {@link #toPrimaryKey} only. */ private transient String lastTableForName; /** * Cache for axis names. This service is not provided by {@link BufferedAuthorityFactory} * since {@link AxisName} object are particular to the EPSG database. * * @see #getAxisName */ private final Map<String,AxisName> axisNames = new HashMap<String,AxisName>(); /** * Cache for axis numbers. This service is not provided by {@link BufferedAuthorityFactory} * since the number of axis is used internally in this class. * * @see #getDimensionForCRS */ private final Map<String,Short> axisCounts = new HashMap<String,Short>(); /** * Cache for projection checks. This service is not provided by {@link BufferedAuthorityFactory} * since the check that a transformation is a projection is used internally in this class. * * @see #isProjection */ private final Map<String,Boolean> codeProjection = new HashMap<String,Boolean>(); /** * Pool of naming systems, used for caching. * There is usually few of them (about 15). */ private final Map<String,LocalName> scopes = new HashMap<String,LocalName>(); /** * The properties to be given the objects to construct. * Reused every time {@link #createProperties} is invoked. */ private final Map<String,Object> properties = new HashMap<String,Object>(); /** * A safety guard for preventing never-ending loops in recursive calls to * {@link #createDatum}. This is used by {@link #createBursaWolfParameters}, * which need to create a target datum. The target datum could have its own * Bursa-Wolf parameters, with one of them pointing again to the source datum. */ private final Set<String> safetyGuard = new HashSet<String>(); public AbstractEpsgFactory(final Hints userHints ) throws FactoryException { super( MAXIMUM_PRIORITY-20 ); // The following hints have no effect on this class behaviour, // but tell to the user what this factory do about axis order. hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE); // // We need to obtain our DataSource if (userHints != null) { Object hint = userHints.get(Hints.EPSG_DATA_SOURCE); if( hint instanceof String ){ String name = (String) hint; try { dataSource = (DataSource) GeoTools.getInitialContext(userHints).lookup(name); } catch (NamingException e) { throw new FactoryException("A EPSG_DATA_SOURCE hint is required:"+e); } hints.put(Hints.EPSG_DATA_SOURCE, dataSource ); } else if( hint instanceof DataSource ){ dataSource = (DataSource) hint; hints.put(Hints.EPSG_DATA_SOURCE, dataSource ); } else { throw new FactoryException("A EPSG_DATA_SOURCE hint is required."); } } else { throw new FactoryException("A EPSG_DATA_SOURCE hint is required."); } } public AbstractEpsgFactory(final Hints userHints, final javax.sql.DataSource dataSource) { super( MAXIMUM_PRIORITY-20 ); this.dataSource = dataSource; // The following hints have no effect on this class behaviour, // but tell to the user what this factory do about axis order. hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE); hints.put(Hints.EPSG_DATA_SOURCE, dataSource ); } /** * Constructs an authority factory using the specified connection. * * @param userHints The underlying factories used for objects creation. * @param connection The connection to the underlying EPSG database. */ public AbstractEpsgFactory(final Hints userHints, final Connection connection) { super(MAXIMUM_PRIORITY-20, userHints ); // The following hints have no effect on this class behaviour, // but tell to the user what this factory do about axis order. hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE); this.connection = connection; ensureNonNull("connection", connection); } /** * Returns the authority for this EPSG database. * This authority will contains the database version in the {@linkplain Citation#getEdition * edition} attribute, together with the {@linkplain Citation#getEditionDate edition date}. */ public synchronized Citation getAuthority() { if (authority == null) try { final String query = adaptSQL("SELECT VERSION_NUMBER, VERSION_DATE FROM [Version History]" + " ORDER BY VERSION_DATE DESC"); final DatabaseMetaData metadata = getConnection().getMetaData(); final Statement statement = getConnection().createStatement(); final ResultSet result = statement.executeQuery(query); if (result.next()) { final String version = result.getString(1); final Date date = result.getDate (2); final String engine = metadata.getDatabaseProductName(); final CitationImpl c = new CitationImpl(Citations.EPSG); c.getAlternateTitles().add(Vocabulary.formatInternational( VocabularyKeys.DATA_BASE_$3, "EPSG", version, engine)); c.setEdition(new SimpleInternationalString(version)); c.setEditionDate(date); authority = (Citation) c.unmodifiable(); hints.put(Hints.VERSION, new Version(version)); // For getImplementationHints() } else { authority = Citations.EPSG; } result.close(); statement.close(); } catch (SQLException exception) { Logging.unexpectedException(LOGGER, AbstractEpsgFactory.class, "getAuthority", exception); return Citations.EPSG; } return authority; } /** * Returns a description of the database engine. * * @throws FactoryException if the database's metadata can't be fetched. */ @Override public synchronized String getBackingStoreDescription() throws FactoryException { final Citation authority = getAuthority(); final TableWriter table = new TableWriter(null, " "); final Vocabulary resources = Vocabulary.getResources(null); CharSequence cs; if ((cs=authority.getEdition()) != null) { table.write(resources.getString(VocabularyKeys.VERSION_OF_$1, "EPSG")); table.write(':'); table.nextColumn(); table.write(cs.toString()); table.nextLine(); } try { String s; final DatabaseMetaData metadata = getConnection().getMetaData(); if ((s=metadata.getDatabaseProductName()) != null) { table.write(resources.getLabel(VocabularyKeys.DATABASE_ENGINE)); table.nextColumn(); table.write(s); if ((s=metadata.getDatabaseProductVersion()) != null) { table.write(' '); table.write(resources.getString(VocabularyKeys.VERSION_$1, s)); } table.nextLine(); } if ((s=metadata.getURL()) != null) { table.write(resources.getLabel(VocabularyKeys.DATABASE_URL)); table.nextColumn(); table.write(s); table.nextLine(); } } catch (SQLException exception) { throw new FactoryException(exception); } return table.toString(); } /** * Returns the implementation hints for this factory. The returned map contains all the * values specified in {@linkplain DirectAuthorityFactory#getImplementationHints subclass}, * with the addition of {@link Hints#VERSION VERSION}. */ @Override public Map<RenderingHints.Key,?> getImplementationHints() { if (authority == null) { // For the computation of Hints.VERSION. getAuthority(); } return super.getImplementationHints(); } /** * Returns the set of authority codes of the given type. * * @param type The spatial reference objects type (may be {@code Object.class}). * @return The set of authority codes for spatial reference objects of the given type. * If this factory doesn't contains any object of the given type, then this method * returns an empty set. * @throws FactoryException if access to the underlying database failed. */ protected synchronized Set<String> generateAuthorityCodes(final Class type) throws FactoryException { Set<String> result = new HashSet<String>(); for (int i=0; i<TABLES_INFO.length; i++) { final TableInfo table = TABLES_INFO[i]; if (table.isTypeOf(type)) { final AuthorityCodeSet codes = new AuthorityCodeSet(TABLES_INFO[i], type); result.addAll(codes); } } return result; } /** * Gets a description of the object corresponding to a code. * * @param code Value allocated by authority. * @return A description of the object, or {@code null} if the object * corresponding to the specified {@code code} has no description. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. * @throws FactoryException if the query failed for some other reason. */ public InternationalString getDescriptionText(final String code) throws FactoryException { IdentifiedObject identifiedObject = createObject(code); final Identifier identifier = identifiedObject.getName(); if (identifier instanceof GenericName) { return ((GenericName) identifier).toInternationalString(); } return new SimpleInternationalString( identifier.getCode() ); } /** * Returns a prepared statement for the specified name. Most {@link PreparedStatement} * creations are performed through this method, except {@link #getNumericalIdentifier} * and {@link #createObject}. * * @param key A key uniquely identifying the caller * (e.g. {@code "Ellipsoid"} for {@link #createEllipsoid}). * @param sql The SQL statement to use if for creating the {@link PreparedStatement} * object. Will be used only if no prepared statement was already created for * the specified key. * @return The prepared statement. * @throws SQLException if the prepared statement can't be created. */ private PreparedStatement prepareStatement(final String key, final String sql) throws SQLException { assert Thread.holdsLock(this); PreparedStatement stmt = statements.get(key); if (stmt == null) { stmt = getConnection().prepareStatement(adaptSQL(sql)); statements.put(key, stmt); } return stmt; } /** * Gets the string from the specified {@link ResultSet}. * The string is required to be non-null. A null string * will throw an exception. * * @param result The result set to fetch value from. * @param columnIndex The column index (1-based). * @param code The identifier of the record where the string was found. * @return The string at the specified column. * @throws SQLException if a SQL error occured. * @throws FactoryException If a null value was found. */ private static String getString(final ResultSet result, final int columnIndex, final String code) throws SQLException, FactoryException { final String value = result.getString(columnIndex); ensureNonNull(result, columnIndex, code); return value.trim(); } /** * Same as {@link #getString(ResultSet,int,String)}, but report the fault on an alternative * column if the value is null. */ private static String getString(final ResultSet result, final int columnIndex, final String code, final int columnFault) throws SQLException, FactoryException { final String str = result.getString(columnIndex); if (result.wasNull()) { final ResultSetMetaData metadata = result.getMetaData(); final String column = metadata.getColumnName(columnFault); final String table = metadata.getTableName (columnFault); result.close(); throw new FactoryException( Errors.format(ErrorKeys.NULL_VALUE_IN_TABLE_$3, code, column, table)); } return str.trim(); } /** * Gets the value from the specified {@link ResultSet}. * The value is required to be non-null. A null value * (i.e. blank) will throw an exception. * * @param result The result set to fetch value from. * @param columnIndex The column index (1-based). * @param code The identifier of the record where the string was found. * @return The double at the specified column. * @throws SQLException if a SQL error occured. * @throws FactoryException If a null value was found. */ private static double getDouble(final ResultSet result, final int columnIndex, final String code) throws SQLException, FactoryException { final double value = result.getDouble(columnIndex); ensureNonNull(result, columnIndex, code); return value; } /** * Gets the value from the specified {@link ResultSet}. * The value is required to be non-null. A null value * (i.e. blank) will throw an exception. * * @param result The result set to fetch value from. * @param columnIndex The column index (1-based). * @param code The identifier of the record where the string was found. * @return The integer at the specified column. * @throws SQLException if a SQL error occured. * @throws FactoryException If a null value was found. */ private static int getInt(final ResultSet result, final int columnIndex, final String code) throws SQLException, FactoryException { final int value = result.getInt(columnIndex); ensureNonNull(result, columnIndex, code); return value; } /** * Make sure that the last result was non-null. Used for {@code getString}, {@code getDouble} * and {@code getInt} methods only. */ private static void ensureNonNull(final ResultSet result, final int columnIndex, final String code) throws SQLException, FactoryException { if (result.wasNull()) { final ResultSetMetaData metadata = result.getMetaData(); final String column = metadata.getColumnName(columnIndex); final String table = metadata.getTableName (columnIndex); result.close(); throw new FactoryException( Errors.format(ErrorKeys.NULL_VALUE_IN_TABLE_$3, code, column, table)); } } /** * Converts a code from an arbitrary name to the numerical identifier (the primary key). * If the supplied code is already a numerical value, then it is returned unchanged. * If the code is not found in the name column, it is returned unchanged as well so that * the caller will produces an appropriate "Code not found" error message. If the code * is found more than once, then an exception is thrown. * <p> * Note that this method includes a call to {@link #trimAuthority}, so there is no need to * call it before or after this method. * * @param type The type of object to create. * @param code The code to check. * @param table The table where the code should appears. * @param codeColumn The column name for the code. * @param nameColumn The column name for the name. * @return The numerical identifier (i.e. the table primary key value). * @throws SQLException if an error occured while reading the database. */ private String toPrimaryKey(final Class type, final String code, final String table, final String codeColumn, final String nameColumn) throws SQLException, FactoryException { assert Thread.holdsLock(this); String identifier = trimAuthority(code); if (!isPrimaryKey(identifier)) { /* * The character is not the numerical code. Search the value in the database. * If a prepared statement is already available, reuse it providing that it was * created for the current table. Otherwise, we will create a new statement. */ final String KEY = "NumericalIdentifier"; PreparedStatement statement = statements.get(KEY); if (statement != null) { if (!table.equals(lastTableForName)) { statements.remove(KEY); statement.close(); statement = null; lastTableForName = null; } } if (statement == null) { final String query = "SELECT " + codeColumn + " FROM " + table + " WHERE " + nameColumn + " = ?"; statement = getConnection().prepareStatement(adaptSQL(query)); statements.put(KEY, statement); } statement.setString(1, identifier); identifier = null; final ResultSet result = statement.executeQuery(); while (result.next()) { identifier = ensureSingleton(result.getString(1), identifier, code); } result.close(); if (identifier == null) { throw noSuchAuthorityCode(type, code); } } return identifier; } /** * Make sure that an object constructed from the database is not incoherent. * If the code supplied to a {@code createFoo} method exists in the database, * then we should find only one record. However, we will do a paranoiac check and * verify if there is more records, using a {@code while (results.next())} * loop instead of {@code if (results.next())}. This method is invoked in * the loop for making sure that, if there is more than one record (which should * never happen), at least they have identical contents. * * @param newValue The newly constructed object. * @param oldValue The object previously constructed, or {@code null} if none. * @param code The EPSG code (for formatting error message). * @throws FactoryException if a duplication has been detected. * * @todo Use generic type when we will be allowed to compile for J2SE 1.5. */ private static <T> T ensureSingleton(final T newValue, final T oldValue, final String code) throws FactoryException { if (oldValue == null) { return newValue; } if (oldValue.equals(newValue)) { return oldValue; } throw new FactoryException(Errors.format(ErrorKeys.DUPLICATED_VALUES_$1, code)); } /** * Returns the name for the {@link IdentifiedObject} to construct. * This method also search for alias. * * @param name The name for the {@link IndentifiedObject} to construct. * @param code The EPSG code of the object to construct. * @param remarks Remarks, or {@code null} if none. * @return The name together with a set of properties. */ private Map<String,Object> generateProperties(final String name, final String code, String remarks) throws SQLException, FactoryException { properties.clear(); final Citation authority = getAuthority(); if (name != null) { properties.put(IdentifiedObject.NAME_KEY, new NamedIdentifier(authority, name.trim())); } if (code != null) { final InternationalString edition = authority.getEdition(); final String version = (edition!=null) ? edition.toString() : null; properties.put(IdentifiedObject.IDENTIFIERS_KEY, new NamedIdentifier(authority, code.trim(), version)); } if (remarks!=null && (remarks=remarks.trim()).length()!=0) { properties.put(IdentifiedObject.REMARKS_KEY, remarks); } /* * Search for alias. */ List<GenericName> alias = null; final PreparedStatement stmt; stmt = prepareStatement("Alias", "SELECT NAMING_SYSTEM_NAME, ALIAS" + " FROM [Alias] INNER JOIN [Naming System]" + " ON [Alias].NAMING_SYSTEM_CODE =" + " [Naming System].NAMING_SYSTEM_CODE" + " WHERE OBJECT_CODE = ?"); stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String scope = result.getString(1); final String local = getString(result, 2, code); final GenericName generic; if (scope == null) { generic = new LocalName(local); } else { LocalName cached = scopes.get(scope); if (cached == null) { cached = new LocalName(scope); scopes.put(scope, cached); } generic = new ScopedName(cached, local); } if (alias == null) { alias = new ArrayList<GenericName>(); } alias.add(generic); } result.close(); if (alias != null) { properties.put(IdentifiedObject.ALIAS_KEY, alias.toArray(new GenericName[alias.size()])); } return properties; } /** * Returns the name for the {@link IdentifiedObject} to construct. * This method also search for alias. * * @param name The name for the {@link IndentifiedObject} to construct. * @param code The EPSG code of the object to construct. * @param area The area of use, or {@code null} if none. * @param scope The scope, or {@code null} if none. * @param remarks Remarks, or {@code null} if none. * @return The name together with a set of properties. */ private Map<String,Object> generateProperties(final String name, final String code, String area, String scope, String remarks) throws SQLException, FactoryException { final Map<String,Object> properties = generateProperties(name, code, remarks); if (area != null && (area=area.trim()).length() != 0) { final Extent extent = generateExtent(area); properties.put(Datum.DOMAIN_OF_VALIDITY_KEY, extent); } if (scope != null && (scope=scope.trim()).length() != 0) { properties.put(Datum.SCOPE_KEY, scope); } return properties; } /** * Returns an arbitrary object from a code. * The default implementation invokes one of {@link #createCoordinateReferenceSystem}, * {@link #createCoordinateSystem}, {@link #createDatum}, {@link #createEllipsoid}, or * {@link #createUnit} methods according the object type. * * @param code The EPSG value. * @return The object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized IdentifiedObject generateObject(final String code) throws FactoryException { ensureNonNull("code", code); final String KEY = "IdentifiedObject"; PreparedStatement stmt = statements.get(KEY); // Null allowed. StringBuilder query = null; // Will be created only if the last statement doesn't suit. /* * Iterates through all tables listed in TABLES_INFO, starting with the table used during * the last call to 'createObject(code)'. This approach assumes that two consecutive calls * will often return the same type of object. If the object type changed, then this method * will have to discard the old prepared statement and prepare a new one, which may be a * costly operation. Only the last successful prepared statement is cached, in order to keep * the amount of statements low. Unsuccessful statements are immediately disposed. */ final String epsg = trimAuthority(code); final boolean isPrimaryKey = isPrimaryKey(epsg); final int tupleToSkip = isPrimaryKey ? lastObjectType : -1; int index = -1; for (int i=-1; i<TABLES_INFO.length; i++) { if (i == tupleToSkip) { // Avoid to test the same table twice. Note that this test also avoid a // NullPointerException if 'stmt' is null, since 'lastObjectType' should // be -1 in this case. continue; } try { if (i >= 0) { final TableInfo table = TABLES_INFO[i]; final String column = isPrimaryKey ? table.codeColumn : table.nameColumn; if (column == null) { continue; } if (query == null) { query = new StringBuilder("SELECT "); } query.setLength(7); // 7 is the length of "SELECT " in the line above. query.append(table.codeColumn); query.append(" FROM "); query.append(table.table); query.append(" WHERE "); query.append(column); query.append(" = ?"); if (isPrimaryKey) { assert !statements.containsKey(KEY) : table; stmt = prepareStatement(KEY, query.toString()); } else { // Do not cache the statement for names. stmt = getConnection().prepareStatement(adaptSQL(query.toString())); } } /* * Checks if at least one record is found for the code. If the code is the primary * key, then we will stop at the first table found since a well-formed EPSG database * should not contains any duplicate identifiers. In the code is a name, then search * in all tables since duplicate names exist. */ stmt.setString(1, epsg); final ResultSet result = stmt.executeQuery(); final boolean present = result.next(); result.close(); if (present) { if (index >= 0) { throw new FactoryException(Errors.format(ErrorKeys.DUPLICATED_VALUES_$1, code)); } index = (i < 0) ? lastObjectType : i; if (isPrimaryKey) { // Don't scan other tables, since primary keys should be unique. // Note that names may be duplicated, so we don't stop for names. break; } } if (isPrimaryKey) { if (statements.remove(KEY) == null) { throw new AssertionError(code); // Should never happen. } } stmt.close(); } catch (SQLException exception) { throw databaseFailure(IdentifiedObject.class, code, exception); } } /* * If a record has been found in one table, then delegates to the appropriate method. */ if (isPrimaryKey) { lastObjectType = index; } if (index >= 0) { switch (index) { case 0: return createCoordinateReferenceSystem(code); case 1: return createCoordinateSystem (code); case 2: return createCoordinateSystemAxis (code); case 3: return createDatum (code); case 4: return createEllipsoid (code); case 5: return createPrimeMeridian (code); case 6: return createCoordinateOperation (code); case 7: return generateOperationMethod (code); case 8: return generateParameterDescriptor (code); case 9: break; // Can't cast Unit to IdentifiedObject default: throw new AssertionError(index); // Should not happen } } return super.createObject(code); } /** * Returns an unit from a code. * * @param code Value allocated by authority. * @return The unit object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized Unit<?> generateUnit(final String code) throws FactoryException { ensureNonNull("code", code); Unit<?> returnValue = null; try { final String primaryKey = toPrimaryKey(Unit.class, code, "[Unit of Measure]", "UOM_CODE", "UNIT_OF_MEAS_NAME"); final PreparedStatement stmt; stmt = prepareStatement("Unit", "SELECT UOM_CODE," + " FACTOR_B," + " FACTOR_C," + " TARGET_UOM_CODE" + " FROM [Unit of Measure]" + " WHERE UOM_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { final int source = getInt(result, 1, code); final double b = result.getDouble(2); final double c = result.getDouble(3); final int target = getInt(result, 4, code); final Unit<?> base = getUnit(target); if (base == null) { throw noSuchAuthorityCode(Unit.class, String.valueOf(target)); } Unit unit = getUnit(source); if (unit != null) { // TODO: check unit consistency here. } else if (b!=0 && c!=0) { unit = (b == c) ? base : base.times(b / c); } else { // TODO: provide a localized message. throw new FactoryException("Unsupported unit: " + code); } returnValue = ensureSingleton(unit, returnValue, code); } result.close(); } catch (SQLException exception) { throw databaseFailure(Unit.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(Unit.class, code); } return returnValue; } /** * Returns an ellipsoid from a code. * * @param code The EPSG value. * @return The ellipsoid object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized Ellipsoid generateEllipsoid(final String code) throws FactoryException { ensureNonNull("code", code); Ellipsoid returnValue = null; try { final String primaryKey = toPrimaryKey(Ellipsoid.class, code, "[Ellipsoid]", "ELLIPSOID_CODE", "ELLIPSOID_NAME"); final PreparedStatement stmt; stmt = prepareStatement("Ellipsoid", "SELECT ELLIPSOID_CODE," + " ELLIPSOID_NAME," + " SEMI_MAJOR_AXIS," + " INV_FLATTENING," + " SEMI_MINOR_AXIS," + " UOM_CODE," + " REMARKS" + " FROM [Ellipsoid]" + " WHERE ELLIPSOID_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { /* * One of 'semiMinorAxis' and 'inverseFlattening' values can be NULL in * the database. Consequently, we don't use 'getString(ResultSet, int)' * because we don't want to thrown an exception if a NULL value is found. */ final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final double semiMajorAxis = getDouble(result, 3, code); final double inverseFlattening = result.getDouble( 4); final double semiMinorAxis = result.getDouble( 5); final String unitCode = getString(result, 6, code); final String remarks = result.getString( 7); final Unit unit = createUnit(unitCode); final Map<String,Object> properties = generateProperties(name, epsg, remarks); final Ellipsoid ellipsoid; if (inverseFlattening == 0) { if (semiMinorAxis == 0) { // Both are null, which is not allowed. final String column = result.getMetaData().getColumnName(3); result.close(); throw new FactoryException(Errors.format(ErrorKeys.NULL_VALUE_IN_TABLE_$3, code, column)); } else { // We only have semiMinorAxis defined -> it's OK ellipsoid = factories.getDatumFactory().createEllipsoid( properties, semiMajorAxis, semiMinorAxis, unit); } } else { if (semiMinorAxis != 0) { // Both 'inverseFlattening' and 'semiMinorAxis' are defined. // Log a warning and create the ellipsoid using the inverse flattening. final LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.AMBIGUOUS_ELLIPSOID,code); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } ellipsoid = factories.getDatumFactory().createFlattenedSphere( properties, semiMajorAxis, inverseFlattening, unit); } /* * Now that we have built an ellipsoid, compare * it with the previous one (if any). */ returnValue = ensureSingleton(ellipsoid, returnValue, code); } result.close(); } catch (SQLException exception) { throw databaseFailure(Ellipsoid.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(Ellipsoid.class, code); } return returnValue; } /** * Returns a prime meridian, relative to Greenwich. * * @param code Value allocated by authority. * @return The prime meridian object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized PrimeMeridian generatePrimeMeridian(final String code) throws FactoryException { ensureNonNull("code", code); PrimeMeridian returnValue = null; try { final String primaryKey = toPrimaryKey(PrimeMeridian.class, code, "[Prime Meridian]", "PRIME_MERIDIAN_CODE", "PRIME_MERIDIAN_NAME"); final PreparedStatement stmt; stmt = prepareStatement("PrimeMeridian", "SELECT PRIME_MERIDIAN_CODE," + " PRIME_MERIDIAN_NAME," + " GREENWICH_LONGITUDE," + " UOM_CODE," + " REMARKS" + " FROM [Prime Meridian]" + " WHERE PRIME_MERIDIAN_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final double longitude = getDouble(result, 3, code); final String unit_code = getString(result, 4, code); final String remarks = result.getString( 5); final Unit unit = createUnit(unit_code); final Map<String,Object> properties = generateProperties(name, epsg, remarks); PrimeMeridian primeMeridian = factories.getDatumFactory().createPrimeMeridian( properties, longitude, unit); returnValue = ensureSingleton(primeMeridian, returnValue, code); } result.close(); } catch (SQLException exception) { throw databaseFailure(PrimeMeridian.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(PrimeMeridian.class, code); } return returnValue; } /** * Returns an area of use. * * @param code Value allocated by authority. * @return The area of use. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized Extent generateExtent(final String code) throws FactoryException { ensureNonNull("code", code); Extent returnValue = null; try { final String primaryKey = toPrimaryKey(Extent.class, code, "[Area]", "AREA_CODE", "AREA_NAME"); final PreparedStatement stmt; stmt = prepareStatement("Area", "SELECT AREA_OF_USE," + " AREA_SOUTH_BOUND_LAT," + " AREA_NORTH_BOUND_LAT," + " AREA_WEST_BOUND_LON," + " AREA_EAST_BOUND_LON" + " FROM [Area]" + " WHERE AREA_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { ExtentImpl extent = null; final String description = result.getString(1); if (description != null) { extent = new ExtentImpl(); extent.setDescription(new SimpleInternationalString(description)); } final double ymin = result.getDouble(2); if (!result.wasNull()) { final double ymax = result.getDouble(3); if (!result.wasNull()) { final double xmin = result.getDouble(4); if (!result.wasNull()) { final double xmax = result.getDouble(5); if (!result.wasNull()) { if (extent == null) { extent = new ExtentImpl(); } extent.setGeographicElements(Collections.singleton( new GeographicBoundingBoxImpl(xmin, xmax, ymin, ymax))); } } } } if (extent != null) { returnValue = (Extent) ensureSingleton(extent.unmodifiable(), returnValue, code); } } result.close(); } catch (SQLException exception) { throw databaseFailure(Extent.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(Extent.class, code); } return returnValue; } /** * Returns Bursa-Wolf parameters for a geodetic datum. If the specified datum has * no conversion informations, then this method will returns {@code null}. * * @param code The EPSG code of the {@link GeodeticDatum}. * @param toClose The result set to close if this method is going to invokes * {@link #createDatum} recursively. This hack is necessary because many * JDBC drivers do not support multiple result sets for the same statement. * The result set is closed if an only if this method returns a non-null value. * @return an array of Bursa-Wolf parameters (in which case {@code toClose} has * been closed), or {@code null} (in which case {@code toClose} has * <strong>not</strong> been closed). */ private BursaWolfParameters[] generateBursaWolfParameters(final String code, final ResultSet toClose) throws SQLException, FactoryException { if (safetyGuard.contains(code)) { /* * Do not try to create Bursa-Wolf parameters if the datum is already * in process of being created. This check avoid never-ending loops in * recursive call to 'createDatum'. */ return null; } PreparedStatement stmt; stmt = prepareStatement("BursaWolfParametersSet", "SELECT CO.COORD_OP_CODE," + " CO.COORD_OP_METHOD_CODE," + " CRS2.DATUM_CODE" + " FROM [Coordinate_Operation] AS CO" + " INNER JOIN [Coordinate Reference System] AS CRS2" + " ON CO.TARGET_CRS_CODE = CRS2.COORD_REF_SYS_CODE" + " WHERE CO.COORD_OP_METHOD_CODE >= " + BURSA_WOLF_MIN_CODE + " AND CO.COORD_OP_METHOD_CODE <= " + BURSA_WOLF_MAX_CODE + " AND CO.COORD_OP_CODE <> " + DUMMY_OPERATION // GEOT-1008 + " AND CO.SOURCE_CRS_CODE IN (" + " SELECT CRS1.COORD_REF_SYS_CODE " // GEOT-1129 + " FROM [Coordinate Reference System] AS CRS1 " + " WHERE CRS1.DATUM_CODE = ?)" + " ORDER BY CRS2.DATUM_CODE," + " ABS(CO.DEPRECATED), CO.COORD_OP_ACCURACY," + " CO.COORD_OP_CODE DESC"); // GEOT-846 fix stmt.setString(1, code); ResultSet result = stmt.executeQuery(); List<Object> bwInfos = null; while (result.next()) { final String operation = getString(result, 1, code); final int method = getInt (result, 2, code); final String datum = getString(result, 3, code); if (bwInfos == null) { bwInfos = new ArrayList<Object>(); } bwInfos.add(new BursaWolfInfo(operation, method, datum)); } result.close(); if (bwInfos == null) { // Don't close the connection here. return null; } toClose.close(); /* * Sorts the infos in preference order. The "ORDER BY" clause above was not enough; * we also need to take the "supersession" table in account. Once the sorting is done, * keep only one Bursa-Wolf parameters for each datum. */ int size = bwInfos.size(); if (size > 1) { final BursaWolfInfo[] codes = bwInfos.toArray(new BursaWolfInfo[size]); sort(codes); bwInfos.clear(); final Set<String> added = new HashSet<String>(); for (int i=0; i<codes.length; i++) { final BursaWolfInfo candidate = codes[i]; if (added.add(candidate.target)) { bwInfos.add(candidate); } } size = bwInfos.size(); } /* * We got all the needed informations before to built Bursa-Wolf parameters because the * 'createDatum(...)' call below may invokes 'createBursaWolfParameters(...)' recursively, * and not all JDBC drivers supported multi-result set for the same statement. Now, iterate * throw the results and fetch the parameter values for each BursaWolfParameters object. */ stmt = prepareStatement("BursaWolfParameters", "SELECT PARAMETER_CODE," + " PARAMETER_VALUE," + " UOM_CODE" + " FROM [Coordinate_Operation Parameter Value]" + " WHERE COORD_OP_CODE = ?" + " AND COORD_OP_METHOD_CODE = ?"); for (int i=0; i<size; i++) { final BursaWolfInfo info = (BursaWolfInfo) bwInfos.get(i); final GeodeticDatum datum; try { safetyGuard.add(code); datum = createGeodeticDatum(info.target); } finally { safetyGuard.remove(code); } final BursaWolfParameters parameters = new BursaWolfParameters(datum); stmt.setString(1, info.operation); stmt.setInt (2, info.method); result = stmt.executeQuery(); while (result.next()) { setBursaWolfParameter(parameters, getInt (result, 1, info.operation), getDouble(result, 2, info.operation), createUnit(getString(result, 3, info.operation))); } result.close(); if (info.method == ROTATION_FRAME_CODE) { // Coordinate frame rotation (9607): same as 9606, // except for the sign of rotation parameters. parameters.ex = -parameters.ex; parameters.ey = -parameters.ey; parameters.ey = -parameters.ey; } bwInfos.set(i, parameters); } return bwInfos.toArray(new BursaWolfParameters[size]); } /** * Returns a datum from a code. * * @param code Value allocated by authority. * @return The datum object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. * * @todo Current implementation maps all "vertical" datum to * {@link VerticalDatumType#GEOIDAL}. We don't know yet how * to maps the exact vertical datum type from the EPSG database. */ public synchronized Datum generateDatum(final String code) throws FactoryException { ensureNonNull("code", code); Datum returnValue = null; try { final String primaryKey = toPrimaryKey(Datum.class, code, "[Datum]", "DATUM_CODE", "DATUM_NAME"); final PreparedStatement stmt; stmt = prepareStatement("Datum", "SELECT DATUM_CODE," + " DATUM_NAME," + " DATUM_TYPE," + " ORIGIN_DESCRIPTION," + " REALIZATION_EPOCH," + " AREA_OF_USE_CODE," + " DATUM_SCOPE," + " REMARKS," + " ELLIPSOID_CODE," // Only for geodetic type + " PRIME_MERIDIAN_CODE" // Only for geodetic type + " FROM [Datum]" + " WHERE DATUM_CODE = ?"); stmt.setString(1, primaryKey); ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String type = getString(result, 3, code).trim().toLowerCase(); final String anchor = result.getString( 4); final String epoch = result.getString( 5); final String area = result.getString( 6); final String scope = result.getString( 7); final String remarks = result.getString( 8); Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); if (anchor != null) { properties.put(Datum.ANCHOR_POINT_KEY, anchor); } if (epoch!=null && epoch.length()!=0) try { calendar.clear(); calendar.set(Integer.parseInt(epoch), 0, 1); properties.put(Datum.REALIZATION_EPOCH_KEY, calendar.getTime()); } catch (NumberFormatException exception) { // Not a fatal error... Logging.unexpectedException(LOGGER, AbstractEpsgFactory.class, "createDatum", exception); } final DatumFactory factory = factories.getDatumFactory(); final Datum datum; /* * Now build datum according their datum type. Constructions are straightforward, * except for the "geodetic" datum type which need some special processing: * * - Because it invokes again 'generateProperties' indirectly (through calls to * 'createEllipsoid' and 'createPrimeMeridian'), it must protect 'properties' * from changes. * * - Because 'createBursaWolfParameters' may invokes 'createDatum' recursively, * we must close the result set if Bursa-Wolf parameters are found. In this * case, we lost our paranoiac check for duplication. */ if (type.equals("geodetic")) { properties = new HashMap<String,Object>(properties); // Protect from changes final Ellipsoid ellipsoid = createEllipsoid (getString(result, 9, code)); final PrimeMeridian meridian = createPrimeMeridian(getString(result, 10, code)); final BursaWolfParameters[] param = generateBursaWolfParameters(primaryKey, result); if (param != null) { result = null; // Already closed by createBursaWolfParameters properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, param); } datum = factory.createGeodeticDatum(properties, ellipsoid, meridian); } else if (type.equals("vertical")) { // TODO: Find the right datum type. datum = factory.createVerticalDatum(properties, VerticalDatumType.GEOIDAL); } else if (type.equals("engineering")) { datum = factory.createEngineeringDatum(properties); } else { result.close(); throw new FactoryException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } returnValue = ensureSingleton(datum, returnValue, code); if (result == null) { // Bypass the 'result.close()' line below: // the ResultSet has already been closed. return returnValue; } } result.close(); } catch (SQLException exception) { throw databaseFailure(Datum.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(Datum.class, code); } return returnValue; } /** * Returns the name and description for the specified {@linkplain CoordinateSystemAxis * coordinate system axis} code. Many axis share the same name and description, so it * is worth to cache them. */ private AxisName getAxisName(final String code) throws FactoryException { assert Thread.holdsLock(this); AxisName returnValue = axisNames.get(code); if (returnValue == null) try { final PreparedStatement stmt; stmt = prepareStatement("AxisName", "SELECT COORD_AXIS_NAME, DESCRIPTION, REMARKS" + " FROM [Coordinate Axis Name]" + " WHERE COORD_AXIS_NAME_CODE = ?"); stmt.setString(1, code); ResultSet result = stmt.executeQuery(); while (result.next()) { final String name = getString(result, 1, code); String description = result.getString (2); String remarks = result.getString (3); if (description == null) { description = remarks; } else if (remarks != null) { description += System.getProperty("line.separator", "\n") + remarks; } final AxisName axis = new AxisName(name, description); returnValue = ensureSingleton(axis, returnValue, code); } result.close(); if (returnValue == null) { throw noSuchAuthorityCode(AxisName.class, code); } axisNames.put(code, returnValue); } catch (SQLException exception) { throw databaseFailure(AxisName.class, code, exception); } return returnValue; } /** * Returns a {@linkplain CoordinateSystemAxis coordinate system axis} from a code. * * @param code Value allocated by authority. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. * @throws FactoryException if the object creation failed for some other reason. */ public synchronized CoordinateSystemAxis generateCoordinateSystemAxis(final String code) throws FactoryException { ensureNonNull("code", code); CoordinateSystemAxis returnValue = null; try { final String primaryKey = trimAuthority(code); final PreparedStatement stmt; stmt = prepareStatement("Axis", "SELECT COORD_AXIS_CODE," + " COORD_AXIS_NAME_CODE," + " COORD_AXIS_ORIENTATION," + " COORD_AXIS_ABBREVIATION," + " UOM_CODE" + " FROM [Coordinate Axis]" + " WHERE COORD_AXIS_CODE = ?"); stmt.setString(1, code); ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String nameCode = getString(result, 2, code); final String orientation = getString(result, 3, code); final String abbreviation = getString(result, 4, code); final String unit = getString(result, 5, code); AxisDirection direction; try { direction = DefaultCoordinateSystemAxis.getDirection(orientation); } catch (NoSuchElementException exception) { if (orientation.equalsIgnoreCase("Geocentre > equator/PM")) { direction = AxisDirection.OTHER; // TODO: can we choose a more accurate direction? } else if (orientation.equalsIgnoreCase("Geocentre > equator/90dE")) { direction = AxisDirection.EAST; } else if (orientation.equalsIgnoreCase("Geocentre > north pole")) { direction = AxisDirection.NORTH; } else { throw new FactoryException(exception); } } final AxisName an = getAxisName(nameCode); final Map<String,Object> properties = generateProperties(an.name, epsg, an.description); final CSFactory factory = factories.getCSFactory(); final CoordinateSystemAxis axis = factory.createCoordinateSystemAxis( properties, abbreviation, direction, createUnit(unit)); returnValue = ensureSingleton(axis, returnValue, code); } result.close(); } catch (SQLException exception) { throw databaseFailure(CoordinateSystemAxis.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(CoordinateSystemAxis.class, code); } return returnValue; } /** * Returns the coordinate system axis from an EPSG code for a {@link CoordinateSystem}. * <p> * <strong>WARNING:</strong> The EPSG database uses "{@code ORDER}" as a column name. * This is tolerated by Access, but MySQL doesn't accept this name. * * @param code the EPSG code for coordinate system owner. * @param dimension of the coordinate system, which is also the size of the returned array. * @return An array of coordinate system axis. * @throws SQLException if an error occured during database access. * @throws FactoryException if the code has not been found. */ private CoordinateSystemAxis[] generateAxisForCoordinateSystem(final String code, final int dimension) throws SQLException, FactoryException { assert Thread.holdsLock(this); final CoordinateSystemAxis[] axis = new CoordinateSystemAxis[dimension]; final PreparedStatement stmt; stmt = prepareStatement("AxisOrder", "SELECT COORD_AXIS_CODE" + " FROM [Coordinate Axis]" + " WHERE COORD_SYS_CODE = ?" + " ORDER BY [ORDER]"); // WARNING: Be careful about the column name : // MySQL rejects ORDER as a column name !!! stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); int i = 0; while (result.next()) { final String axisCode = getString(result, 1, code); if (i < axis.length) { // If 'i' is out of bounds, an exception will be thrown after the loop. // We don't want to thrown an ArrayIndexOutOfBoundsException here. axis[i] = createCoordinateSystemAxis(axisCode); } ++i; } result.close(); if (i != axis.length) { throw new FactoryException(Errors.format( ErrorKeys.MISMATCHED_DIMENSION_$2, axis.length, i)); } return axis; } /** * Returns a coordinate system from a code. * * @param code Value allocated by authority. * @return The coordinate system object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized CoordinateSystem generateCoordinateSystem(final String code) throws FactoryException { ensureNonNull("code", code); CoordinateSystem returnValue = null; final PreparedStatement stmt; try { final String primaryKey = toPrimaryKey(CoordinateSystem.class, code, "[Coordinate System]", "COORD_SYS_CODE", "COORD_SYS_NAME"); stmt = prepareStatement("CoordinateSystem", "SELECT COORD_SYS_CODE," + " COORD_SYS_NAME," + " COORD_SYS_TYPE," + " DIMENSION," + " REMARKS" + " FROM [Coordinate System]" + " WHERE COORD_SYS_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String type = getString(result, 3, code).trim().toLowerCase(); final int dimension = getInt (result, 4, code); final String remarks = result.getString( 5); final CoordinateSystemAxis[] axis = generateAxisForCoordinateSystem(primaryKey, dimension); final Map<String,Object> properties = generateProperties(name, epsg, remarks); // Must be after axis final CSFactory factory = factories.getCSFactory(); CoordinateSystem cs = null; if (type.equals("ellipsoidal")) { switch (dimension) { case 2: cs=factory.createEllipsoidalCS(properties, axis[0], axis[1]); break; case 3: cs=factory.createEllipsoidalCS(properties, axis[0], axis[1], axis[2]); break; } } else if (type.equals("cartesian")) { switch (dimension) { case 2: cs=factory.createCartesianCS(properties, axis[0], axis[1]); break; case 3: cs=factory.createCartesianCS(properties, axis[0], axis[1], axis[2]); break; } } else if (type.equals("spherical")) { switch (dimension) { case 3: cs=factory.createSphericalCS(properties, axis[0], axis[1], axis[2]); break; } } else if (type.equals("vertical") || type.equals("gravity-related")) { switch (dimension) { case 1: cs=factory.createVerticalCS(properties, axis[0]); break; } } else if (type.equals("linear")) { switch (dimension) { case 1: cs=factory.createLinearCS(properties, axis[0]); break; } } else if (type.equals("polar")) { switch (dimension) { case 2: cs=factory.createPolarCS(properties, axis[0], axis[1]); break; } } else if (type.equals("cylindrical")) { switch (dimension) { case 3: cs=factory.createCylindricalCS(properties, axis[0], axis[1], axis[2]); break; } } else if (type.equals("affine")) { switch (dimension) { case 2: cs=factory.createAffineCS(properties, axis[0], axis[1]); break; case 3: cs=factory.createAffineCS(properties, axis[0], axis[1], axis[2]); break; } } else { result.close(); throw new FactoryException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } if (cs == null) { result.close(); throw new FactoryException(Errors.format( ErrorKeys.UNEXPECTED_DIMENSION_FOR_CS_$1, type)); } returnValue = ensureSingleton(cs, returnValue, code); } result.close(); } catch (SQLException exception) { throw databaseFailure(CoordinateSystem.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(CoordinateSystem.class, code); } return returnValue; } /** * Returns the primary key for a coordinate reference system name. * This method is used both by {@link #createCoordinateReferenceSystem} * and {@link #createFromCoordinateReferenceSystemCodes} */ private String toPrimaryKeyCRS(final String code) throws SQLException, FactoryException { return toPrimaryKey(CoordinateReferenceSystem.class, code, "[Coordinate Reference System]", "COORD_REF_SYS_CODE", "COORD_REF_SYS_NAME"); } /** * Returns a coordinate reference system from a code. * * @param code Value allocated by authority. * @return The coordinate reference system object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized CoordinateReferenceSystem generateCoordinateReferenceSystem(final String code) throws FactoryException { ensureNonNull("code", code); CoordinateReferenceSystem returnValue = null; try { final String primaryKey = toPrimaryKeyCRS(code); final PreparedStatement stmt; stmt = prepareStatement("CoordinateReferenceSystem", "SELECT COORD_REF_SYS_CODE," + " COORD_REF_SYS_NAME," + " AREA_OF_USE_CODE," + " CRS_SCOPE," + " REMARKS," + " COORD_REF_SYS_KIND," + " COORD_SYS_CODE," // Null for CompoundCRS + " DATUM_CODE," // Null for ProjectedCRS + " SOURCE_GEOGCRS_CODE," // For ProjectedCRS + " PROJECTION_CONV_CODE," // For ProjectedCRS + " CMPD_HORIZCRS_CODE," // For CompoundCRS only + " CMPD_VERTCRS_CODE" // For CompoundCRS only + " FROM [Coordinate Reference System]" + " WHERE COORD_REF_SYS_CODE = ?"); stmt.setString(1, primaryKey); ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String area = result.getString( 3); final String scope = result.getString( 4); final String remarks = result.getString( 5); final String type = getString(result, 6, code); // Note: Do not invoke 'generateProperties' now, even if we have all required // informations, because the 'properties' map is going to overwritten // by calls to 'createDatum', 'createCoordinateSystem', etc. final CRSFactory factory = factories.getCRSFactory(); final CoordinateReferenceSystem crs; /* ---------------------------------------------------------------------- * GEOGRAPHIC CRS * * NOTE: 'generateProperties' MUST be invoked after any call to an other * 'createFoo' method. Consequently, do not factor out. * ---------------------------------------------------------------------- */ if (type.equalsIgnoreCase("geographic 2D") || type.equalsIgnoreCase("geographic 3D")) { final String csCode = getString(result, 7, code); final String dmCode = result.getString( 8); final EllipsoidalCS cs = createEllipsoidalCS(csCode); final GeodeticDatum datum; if (dmCode != null) { datum = createGeodeticDatum(dmCode); } else { final String geoCode = getString(result, 9, code, 8); result.close(); // Must be close before createGeographicCRS result = null; final GeographicCRS baseCRS = createGeographicCRS(geoCode); datum = baseCRS.getDatum(); } final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); crs = factory.createGeographicCRS(properties, datum, cs); } /* ---------------------------------------------------------------------- * PROJECTED CRS * * NOTE: This method invokes itself indirectly, through createGeographicCRS. * Consequently, we can't use 'result' anymore. We must close it here. * ---------------------------------------------------------------------- */ else if (type.equalsIgnoreCase("projected")) { final String csCode = getString(result, 7, code); final String geoCode = getString(result, 9, code); final String opCode = getString(result, 10, code); result.close(); // Must be close before createGeographicCRS result = null; final CartesianCS cs = createCartesianCS(csCode); final GeographicCRS baseCRS = createGeographicCRS(geoCode); final CoordinateOperation op = createCoordinateOperation(opCode); if (op instanceof Conversion) { final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); crs = factory.createProjectedCRS(properties, baseCRS, (Conversion)op, cs); } else { throw noSuchAuthorityCode(Projection.class, opCode); } } /* ---------------------------------------------------------------------- * VERTICAL CRS * ---------------------------------------------------------------------- */ else if (type.equalsIgnoreCase("vertical")) { final String csCode = getString(result, 7, code); final String dmCode = getString(result, 8, code); final VerticalCS cs = createVerticalCS (csCode); final VerticalDatum datum = createVerticalDatum(dmCode); final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); crs = factory.createVerticalCRS(properties, datum, cs); } /* ---------------------------------------------------------------------- * COMPOUND CRS * * NOTE: This method invokes itself recursively. * Consequently, we can't use 'result' anymore. * ---------------------------------------------------------------------- */ else if (type.equalsIgnoreCase("compound")) { final String code1 = getString(result, 11, code); final String code2 = getString(result, 12, code); result.close(); result = null; final CoordinateReferenceSystem crs1, crs2; if (!safetyGuard.add(epsg)) { throw recursiveCall(CompoundCRS.class, epsg); } try { crs1 = createCoordinateReferenceSystem(code1); crs2 = createCoordinateReferenceSystem(code2); } finally { safetyGuard.remove(epsg); } // Note: Don't invoke 'generateProperties' sooner. final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); crs = factory.createCompoundCRS(properties, new CoordinateReferenceSystem[] {crs1, crs2}); } /* ---------------------------------------------------------------------- * GEOCENTRIC CRS * ---------------------------------------------------------------------- */ else if (type.equalsIgnoreCase("geocentric")) { final String csCode = getString(result, 7, code); final String dmCode = getString(result, 8, code); final CoordinateSystem cs = createCoordinateSystem(csCode); final GeodeticDatum datum = createGeodeticDatum (dmCode); final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); if (cs instanceof CartesianCS) { crs = factory.createGeocentricCRS(properties, datum, (CartesianCS) cs); } else if (cs instanceof SphericalCS) { crs = factory.createGeocentricCRS(properties, datum, (SphericalCS) cs); } else { result.close(); throw new FactoryException(Errors.format( ErrorKeys.ILLEGAL_COORDINATE_SYSTEM_FOR_CRS_$2, cs.getClass(), GeocentricCRS.class)); } } /* ---------------------------------------------------------------------- * ENGINEERING CRS * ---------------------------------------------------------------------- */ else if (type.equalsIgnoreCase("engineering")) { final String csCode = getString(result, 7, code); final String dmCode = getString(result, 8, code); final CoordinateSystem cs = createCoordinateSystem(csCode); final EngineeringDatum datum = createEngineeringDatum(dmCode); final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); crs = factory.createEngineeringCRS(properties, datum, cs); } /* ---------------------------------------------------------------------- * UNKNOW CRS * ---------------------------------------------------------------------- */ else { result.close(); throw new FactoryException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } returnValue = ensureSingleton(crs, returnValue, code); if (result == null) { // Bypass the 'result.close()' line below: // the ResultSet has already been closed. return returnValue; } } result.close(); } catch (SQLException exception) { throw databaseFailure(CoordinateReferenceSystem.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(CoordinateReferenceSystem.class, code); } return returnValue; } /** * Returns a parameter descriptor from a code. * * @param code The parameter descriptor code allocated by EPSG authority. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized ParameterDescriptor generateParameterDescriptor(final String code) throws FactoryException { ensureNonNull("code", code); ParameterDescriptor returnValue = null; final PreparedStatement stmt; try { final String primaryKey = toPrimaryKey(ParameterDescriptor.class, code, "[Coordinate_Operation Parameter]", "PARAMETER_CODE", "PARAMETER_NAME"); stmt = prepareStatement("ParameterDescriptor", // Must be singular form. "SELECT PARAMETER_CODE," + " PARAMETER_NAME," + " DESCRIPTION" + " FROM [Coordinate_Operation Parameter]" + " WHERE PARAMETER_CODE = ?"); stmt.setString(1, primaryKey); ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String remarks = result.getString( 3); final Unit unit; final Class type; /* * Search for units. We will choose the most commonly used one in parameter values. * If the parameter appears to have at least one non-null value in the "Parameter * File Name" column, then the type is assumed to be URI. Otherwise, the type is a * floating point number. */ final PreparedStatement units = prepareStatement("ParameterUnit", "SELECT MIN(UOM_CODE) AS UOM," + " MIN(PARAM_VALUE_FILE_REF) AS FILEREF" + " FROM [Coordinate_Operation Parameter Value]" + " WHERE (PARAMETER_CODE = ?)" + " GROUP BY UOM_CODE" + " ORDER BY COUNT(UOM_CODE) DESC"); units.setString(1, epsg); final ResultSet resultUnits = units.executeQuery(); if (resultUnits.next()) { String element = resultUnits.getString(1); unit = (element!=null) ? createUnit(element) : null; element = resultUnits.getString(2); type = (element!=null && element.trim().length()!=0) ? URI.class : double.class; } else { unit = null; type = double.class; } resultUnits.close(); /* * Now creates the parameter descriptor. */ final ParameterDescriptor descriptor; final Map<String,Object> properties = generateProperties(name, epsg, remarks); descriptor = new DefaultParameterDescriptor(properties, type, null, null, null, null, unit, true); returnValue = ensureSingleton(descriptor, returnValue, code); } } catch (SQLException exception) { throw databaseFailure(OperationMethod.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(OperationMethod.class, code); } return returnValue; } /** * Returns all parameter descriptors for the specified method. * * @param method The operation method code. * @return The parameter descriptors. * @throws SQLException if a SQL statement failed. */ private ParameterDescriptor[] generateParameterDescriptors(final String method) throws FactoryException, SQLException { final PreparedStatement stmt; stmt = prepareStatement("ParameterDescriptors", // Must be plural form. "SELECT PARAMETER_CODE" + " FROM [Coordinate_Operation Parameter Usage]" + " WHERE COORD_OP_METHOD_CODE = ?" + " ORDER BY SORT_ORDER"); stmt.setString(1, method); final ResultSet results = stmt.executeQuery(); final List<ParameterDescriptor> descriptors = new ArrayList<ParameterDescriptor>(); while (results.next()) { final String param = getString(results, 1, method); descriptors.add(generateParameterDescriptor(param)); } results.close(); return descriptors.toArray(new ParameterDescriptor[descriptors.size()]); } /** * Fill parameter values in the specified group. * * @param method The EPSG code for the operation method. * @param operation The EPSG code for the operation (conversion or transformation). * @param value The parameter values to fill. * @throws SQLException if a SQL statement failed. */ private void fillParameterValues(final String method, final String operation, final ParameterValueGroup parameters) throws FactoryException, SQLException { final PreparedStatement stmt; stmt = prepareStatement("ParameterValues", "SELECT CP.PARAMETER_NAME," + " CV.PARAMETER_VALUE," + " CV.PARAM_VALUE_FILE_REF," + " CV.UOM_CODE" + " FROM ([Coordinate_Operation Parameter Value] AS CV" + " INNER JOIN [Coordinate_Operation Parameter] AS CP" + " ON CV.PARAMETER_CODE = CP.PARAMETER_CODE)" + " INNER JOIN [Coordinate_Operation Parameter Usage] AS CU" + " ON (CP.PARAMETER_CODE = CU.PARAMETER_CODE)" + " AND (CV.COORD_OP_METHOD_CODE = CU.COORD_OP_METHOD_CODE)" + " WHERE CV.COORD_OP_METHOD_CODE = ?" + " AND CV.COORD_OP_CODE = ?" + " ORDER BY CU.SORT_ORDER"); stmt.setString(1, method); stmt.setString(2, operation); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String name = getString(result, 1, operation); final double value = result.getDouble( 2); final Unit unit; Object reference; if (result.wasNull()) { /* * If no numeric values were provided in the database, then the values must * appears in some external file. It may be a file to download from FTP. */ reference = getString(result, 3, operation); try { reference = new URI((String) reference); } catch (URISyntaxException exception) { // Ignore: we will stores the reference as a file. reference = new File((String) reference); } unit = null; } else { reference = null; final String unitCode = result.getString(4); unit = (unitCode!=null) ? createUnit(unitCode) : null; } final ParameterValue param; try { param = parameters.parameter(name); } catch (ParameterNotFoundException exception) { /* * Wraps the unchecked ParameterNotFoundException into the checked * NoSuchIdentifierException, which is a FactoryException subclass. * Note that in theory, NoSuchIdentifierException is for MathTransforms rather * than parameters. However, we are close in spirit here since we are setting * up MathTransform's parameters. Using NoSuchIdentifierException allows users * (including CoordinateOperationSet) to know that the failure is probably * caused by a MathTransform not yet supported in Geotools (or only partially * supported) rather than some more serious failure in the database side. * CoordinateOperationSet uses this information in order to determine if it * should try the next coordinate operation or propagate the exception. */ final NoSuchIdentifierException e = new NoSuchIdentifierException( Errors.format(ErrorKeys.CANT_SET_PARAMETER_VALUE_$1, name), name); e.initCause(exception); throw e; } try { if (reference != null) { param.setValue(reference); } else if (unit != null) { param.setValue(value, unit); } else { param.setValue(value); } } catch (InvalidParameterValueException exception) { throw new FactoryException( Errors.format(ErrorKeys.CANT_SET_PARAMETER_VALUE_$1, name), exception); } } result.close(); } /** * Returns an operation method from a code. * * @param code The operation method code allocated by EPSG authority. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized OperationMethod generateOperationMethod(final String code) throws FactoryException { ensureNonNull("code", code); OperationMethod returnValue = null; final PreparedStatement stmt; try { final String primaryKey = toPrimaryKey(OperationMethod.class, code, "[Coordinate_Operation Method]", "COORD_OP_METHOD_CODE", "COORD_OP_METHOD_NAME"); stmt = prepareStatement("OperationMethod", "SELECT COORD_OP_METHOD_CODE," + " COORD_OP_METHOD_NAME," + " FORMULA," + " REMARKS" + " FROM [Coordinate_Operation Method]" + " WHERE COORD_OP_METHOD_CODE = ?"); stmt.setString(1, primaryKey); final ResultSet result = stmt.executeQuery(); OperationMethod method = null; while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String formula = result.getString( 3); final String remarks = result.getString( 4); final int encoded = getDimensionsForMethod(epsg); final int sourceDimensions = encoded >>> 16; final int targetDimensions = encoded & 0xFFFF; final ParameterDescriptor[] descriptors = generateParameterDescriptors(epsg); final Map<String,Object> properties = generateProperties(name, epsg, remarks); if (formula != null) { properties.put(OperationMethod.FORMULA_KEY, formula); } method = new DefaultOperationMethod(properties, sourceDimensions, targetDimensions, new DefaultParameterDescriptorGroup(properties, descriptors)); returnValue = ensureSingleton(method, returnValue, code); } } catch (SQLException exception) { throw databaseFailure(OperationMethod.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(OperationMethod.class, code); } return returnValue; } /** * Returns the must common source and target dimensions for the specified method. * Source dimension is encoded in the 16 highest bits and target dimension is encoded * in the 16 lowest bits. If this method can't infers the dimensions from the "Coordinate * Operation" table, then the operation method is probably a projection, which always have * (2,2) dimensions in the EPSG database. */ private int getDimensionsForMethod(final String code) throws SQLException { final PreparedStatement stmt; stmt = prepareStatement("MethodDimensions", "SELECT SOURCE_CRS_CODE," + " TARGET_CRS_CODE" + " FROM [Coordinate_Operation]" + " WHERE COORD_OP_METHOD_CODE = ?" + " AND SOURCE_CRS_CODE IS NOT NULL" + " AND TARGET_CRS_CODE IS NOT NULL"); stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); final Map<Dimensions,Dimensions> dimensions = new HashMap<Dimensions,Dimensions>(); final Dimensions temp = new Dimensions((2 << 16) | 2); // Default to (2,2) dimensions. Dimensions max = temp; while (result.next()) { final short sourceDimensions = getDimensionForCRS(result.getString(1)); final short targetDimensions = getDimensionForCRS(result.getString(2)); temp.encoded = (sourceDimensions << 16) | (targetDimensions); Dimensions candidate = dimensions.get(temp); if (candidate == null) { candidate = new Dimensions(temp.encoded); dimensions.put(candidate, candidate); } if (++candidate.occurences > max.occurences) { max = candidate; } } result.close(); return max.encoded; } /** A counter for source and target dimensions (to be kept together). */ private static final class Dimensions { /** The dimensions as an encoded value. */ int encoded; /** The occurences of this dimensions. */ int occurences; Dimensions(final int e) { encoded = e; } @Override public int hashCode() { return encoded; } @Override public boolean equals(final Object object) { // MUST ignore 'occurences'. return (object instanceof Dimensions) && ((Dimensions) object).encoded == encoded; } @Override public String toString() { return "[(" + (encoded >>> 16) + ',' + (encoded & 0xFFFF) + ")\u00D7" + occurences + ']'; } } /** * Returns the dimension of the specified CRS. If the CRS is not found (which should not * happen, but we don't need to be strict here), then this method assumes a two-dimensional * CRS. */ private short getDimensionForCRS(final String code) throws SQLException { final PreparedStatement stmt; final Short cached = axisCounts.get(code); final short dimension; if (cached == null) { stmt = prepareStatement("Dimension", " SELECT COUNT(COORD_AXIS_CODE)" + " FROM [Coordinate Axis]" + " WHERE COORD_SYS_CODE = (SELECT COORD_SYS_CODE " + " FROM [Coordinate Reference System]" + " WHERE COORD_REF_SYS_CODE = ?)"); stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); dimension = result.next() ? result.getShort(1) : 2; axisCounts.put(code, dimension); result.close(); } else { dimension = cached.shortValue(); } return dimension; } /** * Returns {@code true} if the {@linkplain CoordinateOperation coordinate operation} for the * specified code is a {@linkplain Projection projection}. The caller must have ensured that * the designed operation is a {@linkplain Conversion conversion} before to invoke this method. */ final synchronized boolean isProjection(final String code) throws SQLException { final PreparedStatement stmt; Boolean projection = (Boolean) codeProjection.get(code); if (projection == null) { stmt = prepareStatement("isProjection", "SELECT COORD_REF_SYS_CODE" + " FROM [Coordinate Reference System]" + " WHERE PROJECTION_CONV_CODE = ?" + " AND COORD_REF_SYS_KIND LIKE 'projected%'"); stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); final boolean found = result.next(); result.close(); projection = Boolean.valueOf(found); codeProjection.put(code, projection); } return projection.booleanValue(); } /** * Returns a coordinate operation from a code. * The returned object will either be a {@linkplain Conversion conversion} or a * {@linkplain Transformation transformation}, depending on the code. * * @param code Value allocated by authority. * @return The coordinate operation object. * @throws NoSuchAuthorityCodeException if this method can't find the requested code. * @throws FactoryException if some other kind of failure occured in the backing * store. This exception usually have {@link SQLException} as its cause. */ public synchronized CoordinateOperation generateCoordinateOperation(final String code) throws FactoryException { ensureNonNull("code", code); CoordinateOperation returnValue = null; try { final String primaryKey = toPrimaryKey(CoordinateOperation.class, code, "[Coordinate_Operation]", "COORD_OP_CODE", "COORD_OP_NAME"); final PreparedStatement stmt; stmt = prepareStatement("CoordinateOperation", "SELECT COORD_OP_CODE," + " COORD_OP_NAME," + " COORD_OP_TYPE," + " SOURCE_CRS_CODE," + " TARGET_CRS_CODE," + " COORD_OP_METHOD_CODE," + " COORD_TFM_VERSION," + " COORD_OP_ACCURACY," + " AREA_OF_USE_CODE," + " COORD_OP_SCOPE," + " REMARKS" + " FROM [Coordinate_Operation]" + " WHERE COORD_OP_CODE = ?"); stmt.setString(1, primaryKey); ResultSet result = stmt.executeQuery(); while (result.next()) { final String epsg = getString(result, 1, code); final String name = getString(result, 2, code); final String type = getString(result, 3, code).trim().toLowerCase(); final boolean isTransformation = type.equals("transformation"); final boolean isConversion = type.equals("conversion"); final boolean isConcatenated = type.equals("concatenated operation"); final String sourceCode, targetCode, methodCode; if (isConversion) { // Optional for conversions, mandatory for all others. sourceCode = result.getString(4); targetCode = result.getString(5); } else { sourceCode = getString(result, 4, code); targetCode = getString(result, 5, code); } if (isConcatenated) { // Not applicable to concatenated operation, mandatory for all others. methodCode = result.getString(6); } else { methodCode = getString(result, 6, code); } String version = result.getString( 7); double accuracy = result.getDouble( 8); if (result.wasNull()) accuracy=Double.NaN; String area = result.getString( 9); String scope = result.getString(10); String remarks = result.getString(11); /* * Gets the source and target CRS. They are mandatory for transformations (it * was checked above in this method) and optional for conversions. Conversions * are usually "defining conversions" and don't define source and target CRS. * In EPSG database 6.7, all defining conversions are projections and their * dimensions are always 2. However, this is not generalizable to other kind * of operation methods. For example the "Geocentric translation" operation * method has 3-dimensional source and target. */ final int sourceDimensions, targetDimensions; final CoordinateReferenceSystem sourceCRS, targetCRS; if (sourceCode != null) { sourceCRS = createCoordinateReferenceSystem(sourceCode); sourceDimensions = sourceCRS.getCoordinateSystem().getDimension(); } else { sourceCRS = null; sourceDimensions = 2; // Acceptable default for projections only. } if (targetCode != null) { targetCRS = createCoordinateReferenceSystem(targetCode); targetDimensions = targetCRS.getCoordinateSystem().getDimension(); } else { targetCRS = null; targetDimensions = 2; // Acceptable default for projections only. } /* * Gets the operation method. This is mandatory for conversions and transformations * (it was checked above in this method) but optional for concatenated operations. * Fetching parameter values is part of this block. */ final boolean isBursaWolf; OperationMethod method; final ParameterValueGroup parameters; if (methodCode == null) { isBursaWolf = false; method = null; parameters = null; } else { final int num; try { num = Integer.parseInt(methodCode); } catch (NumberFormatException exception) { result.close(); throw new FactoryException(exception); } isBursaWolf = (num>=BURSA_WOLF_MIN_CODE && num<=BURSA_WOLF_MAX_CODE); // Reminder: The source and target dimensions MUST be computed when // the information is available. Dimension is not always 2!! method = generateOperationMethod(methodCode); if (method.getSourceDimensions() != sourceDimensions || method.getTargetDimensions() != targetDimensions) { method = new DefaultOperationMethod(method, sourceDimensions, targetDimensions); } /* * Note that some parameters required for MathTransform creation are implicit in * the EPSG database (e.g. semi-major and semi-minor axis length in the case of * map projections). We ask the parameter value group straight from the math * transform factory instead of from the operation method in order to get all * required parameter descriptors, including implicit ones. */ final String classe = method.getName().getCode(); parameters = factories.getMathTransformFactory().getDefaultParameters(classe); fillParameterValues(methodCode, epsg, parameters); } /* * Creates common properties. The 'version' and 'accuracy' are usually defined * for transformations only. However, we check them for all kind of operations * (including conversions) and copy the information inconditionnaly if present. * * NOTE: This block must be executed last before object creations below, because * methods like createCoordinateReferenceSystem and createOperationMethod * overwrite the properties map. */ final Map<String,Object> properties = generateProperties(name, epsg, area, scope, remarks); if (version!=null && (version=version.trim()).length()!=0) { properties.put(CoordinateOperation.OPERATION_VERSION_KEY, version); } if (!Double.isNaN(accuracy)) { final QuantitativeResultImpl accuracyResult; final AbsoluteExternalPositionalAccuracyImpl accuracyElement; accuracyResult = new QuantitativeResultImpl(new double[]{accuracy}); // TODO: Need to invoke something equivalent to: // accuracyResult.setValueType(Float.class); // This is the type declared in the MS-Access database. accuracyResult.setValueUnit(SI.METER); // In meters by definition in the EPSG database. accuracyElement = new AbsoluteExternalPositionalAccuracyImpl(accuracyResult); accuracyElement.setMeasureDescription(TRANSFORMATION_ACCURACY); accuracyElement.setEvaluationMethodType(EvaluationMethodType.DIRECT_EXTERNAL); properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[] { (PositionalAccuracy)accuracyElement.unmodifiable() }); } /* * Creates the operation. Conversions should be the only operations allowed to * have null source and target CRS. In such case, the operation is a defining * conversion (usually to be used later as part of a ProjectedCRS creation), * and always a projection in the specific case of the EPSG database (which * allowed us to assume 2-dimensional operation method in the code above for * this specific case - not to be generalized to the whole EPSG database). */ final CoordinateOperation operation; if (isConversion && (sourceCRS==null || targetCRS==null)) { // Note: we usually can't resolve sourceCRS and targetCRS because there // is many of them for the same coordinate operation (projection) code. operation = new DefiningConversion(properties, method, parameters); } else if (isConcatenated) { /* * Concatenated operation: we need to close the current result set, because * we are going to invoke this method recursively in the following lines. * * Note: we instantiate directly the Geotools's implementation of * ConcatenatedOperation instead of using CoordinateOperationFactory in order * to avoid loading the quite large Geotools's implementation of this factory, * and also because it is not part of FactoryGroup anyway. */ result.close(); result = null; final PreparedStatement cstmt = prepareStatement("ConcatenatedOperation", "SELECT SINGLE_OPERATION_CODE" + " FROM [Coordinate_Operation Path]" + " WHERE (CONCAT_OPERATION_CODE = ?)" + " ORDER BY OP_PATH_STEP"); cstmt.setString(1, epsg); final ResultSet cr = cstmt.executeQuery(); final List<String> codes = new ArrayList<String>(); while (cr.next()) { codes.add(cr.getString(1)); } cr.close(); final CoordinateOperation[] operations = new CoordinateOperation[codes.size()]; if (!safetyGuard.add(epsg)) { throw recursiveCall(ConcatenatedOperation.class, epsg); } try { for (int i=0; i<operations.length; i++) { operations[i] = createCoordinateOperation(codes.get(i)); } } finally { safetyGuard.remove(epsg); } try { return new DefaultConcatenatedOperation(properties, operations); } catch (IllegalArgumentException exception) { // May happen if there is less than 2 operations to concatenate. // It happen for some deprecated CRS like 8658 for example. throw new FactoryException(exception); } } else { /* * Needs to create a math transform. A special processing is performed for * datum shift methods, since the conversion from ellipsoid to geocentric * for "geocentric translations" is implicit in the EPSG database. Even in * the case of Molodenski transforms, the axis length to set are the same. */ if (isBursaWolf) try { Ellipsoid ellipsoid = CRSUtilities.getHeadGeoEllipsoid(sourceCRS); if (ellipsoid != null) { final Unit axisUnit = ellipsoid.getAxisUnit(); parameters.parameter("src_semi_major").setValue(ellipsoid.getSemiMajorAxis(), axisUnit); parameters.parameter("src_semi_minor").setValue(ellipsoid.getSemiMinorAxis(), axisUnit); parameters.parameter("src_dim").setValue(sourceCRS.getCoordinateSystem().getDimension()); } ellipsoid = CRSUtilities.getHeadGeoEllipsoid(targetCRS); if (ellipsoid != null) { final Unit axisUnit = ellipsoid.getAxisUnit(); parameters.parameter("tgt_semi_major").setValue(ellipsoid.getSemiMajorAxis(), axisUnit); parameters.parameter("tgt_semi_minor").setValue(ellipsoid.getSemiMinorAxis(), axisUnit); parameters.parameter("tgt_dim").setValue(targetCRS.getCoordinateSystem().getDimension()); } } catch (ParameterNotFoundException exception) { result.close(); throw new FactoryException(Errors.format( ErrorKeys.GEOTOOLS_EXTENSION_REQUIRED_$1, method.getName().getCode(), exception)); } /* * At this stage, the parameters are ready for use. Creates the math transform * and wraps it in the final operation (a Conversion or a Transformation). */ final Class expected; if (isTransformation) { expected = Transformation.class; } else if (isConversion) { expected = Conversion.class; } else { result.close(); throw new FactoryException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } final MathTransform mt = factories.getMathTransformFactory().createBaseToDerived( sourceCRS, parameters, targetCRS.getCoordinateSystem()); // TODO: uses GeoAPI factory method once available. operation = DefaultOperation.create(properties, sourceCRS, targetCRS, mt, method, expected); } returnValue = ensureSingleton(operation, returnValue, code); if (result == null) { // Bypass the 'result.close()' line below: // the ResultSet has already been closed. return returnValue; } } result.close(); } catch (SQLException exception) { throw databaseFailure(CoordinateOperation.class, code, exception); } if (returnValue == null) { throw noSuchAuthorityCode(CoordinateOperation.class, code); } return returnValue; } /** * Creates operations from coordinate reference system codes. * The returned set is ordered with the most accurate operations first. * * @param sourceCode Coded value of source coordinate reference system. * @param targetCode Coded value of target coordinate reference system. * @throws FactoryException if the object creation failed. * * @todo The ordering is not consistent among all database software, because the "accuracy" * column may contains null values. When used in an "ORDER BY" clause, PostgreSQL put * null values last, while Access and HSQL put them first. The PostgreSQL's behavior is * better for what we want (put operations with unknow accuracy last). Unfortunatly, * I don't know yet how to instruct Access to put null values last using standard SQL * ("IIF" is not standard, and Access doesn't seem to understand "CASE ... THEN" clauses). */ public synchronized Set generateFromCoordinateReferenceSystemCodes(final String sourceCode, final String targetCode) throws FactoryException { ensureNonNull("sourceCode", sourceCode); ensureNonNull("targetCode", targetCode); final String pair = sourceCode + " \u21E8 " + targetCode; final CoordinateOperationSet set = new CoordinateOperationSet(this); try { final String sourceKey = toPrimaryKeyCRS(sourceCode); final String targetKey = toPrimaryKeyCRS(targetCode); boolean searchTransformations = false; do { /* * This 'do' loop is executed twice: the first time for searching defining * conversions, and the second time for searching all other kind of operations. * Defining conversions are searched first because they are, by definition, the * most accurate operations. */ final String key, sql; if (searchTransformations) { key = "TransformationFromCRS"; sql = "SELECT COORD_OP_CODE" + " FROM [Coordinate_Operation]" + " WHERE SOURCE_CRS_CODE = ?" + " AND TARGET_CRS_CODE = ?" + " ORDER BY ABS(DEPRECATED), COORD_OP_ACCURACY"; } else { key = "ConversionFromCRS"; sql = "SELECT PROJECTION_CONV_CODE" + " FROM [Coordinate Reference System]" + " WHERE SOURCE_GEOGCRS_CODE = ?" + " AND COORD_REF_SYS_CODE = ?"; } final PreparedStatement stmt = prepareStatement(key, sql); stmt.setString(1, sourceKey); stmt.setString(2, targetKey); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String code = getString(result, 1, pair); set.addAuthorityCode(code, searchTransformations ? null : targetKey); } result.close(); } while ((searchTransformations = !searchTransformations) == true); /* * Search finished. We may have a lot of coordinate operations * (e.g. about 40 for "ED50" (EPSG:4230) to "WGS 84" (EPSG:4326)). * Alter the ordering using the information supplied in the supersession table. */ final String[] codes = set.getAuthorityCodes(); sort(codes); set.setAuthorityCodes(codes); } catch (SQLException exception) { throw databaseFailure(CoordinateOperation.class, pair, exception); } /* * Before to return the set, tests the creation of 1 object in order to report early * (i.e. now) any problems with SQL statements. Remaining operations will be created * only when first needed. */ set.resolve(1); return set; } /** * Sorts an array of codes in preference order. This method orders pairwise the codes according * the information provided in the supersession table. If the same object is superseded by more * than one object, then the most recent one is inserted first. Except for the codes moved as a * result of pairwise ordering, this method try to preserve the old ordering of the supplied * codes (since deprecated operations should already be last). The ordering is performed in * place. * * @param codes The codes, usually as an array of {@link String}. If the array do not contains * string objects, then the {@link Object#toString} method must returns the code * for each element. */ // TODO: Use generic type for "Object[] codes" with J2SE 1.5. private void sort(final Object[] codes) throws SQLException, FactoryException { if (codes.length <= 1) { return; // Nothing to sort. } final PreparedStatement stmt; stmt = prepareStatement("Supersession", "SELECT SUPERSEDED_BY" + " FROM [Supersession]" + " WHERE OBJECT_CODE = ?" + " ORDER BY SUPERSESSION_YEAR DESC"); int maxIterations = 15; // For avoiding never-ending loop. do { boolean changed = false; for (int i=0; i<codes.length; i++) { final String code = codes[i].toString(); stmt.setString(1, code); final ResultSet result = stmt.executeQuery(); while (result.next()) { final String replacement = getString(result, 1, code); for (int j=i+1; j<codes.length; j++) { final Object candidate = codes[j]; if (replacement.equals(candidate.toString())) { /* * Found a code to move in front of the superceded one. */ System.arraycopy(codes, i, codes, i+1, j-i); codes[i++] = candidate; changed = true; } } } result.close(); } if (!changed) { return; } } while (--maxIterations != 0); LOGGER.finer("Possible recursivity in supersessions."); } /** * An implementation of {@link IdentifiedObjectFinder} which scans over a smaller set * of authority codes. * <p> * <b>Implementation note:</b> Since this method may be invoked indirectly by * {@link LongitudeFirstFactory}, it must be insensitive to axis order. */ private final class Finder extends IdentifiedObjectFinder { /** * Creates a new finder backed by the specified <em>buffered</em> authority factory. */ Finder( final Class/*<? extends IdentifiedObject>*/ type) { super( AbstractEpsgFactory.this, type); } /** * Returns a set of authority codes that <strong>may</strong> identify the same object * than the specified one. This implementation tries to get a smaller set than what * {@link AbstractEpsgFactory#getAuthorityCodes} would produce. */ @Override protected Set getCodeCandidates(final IdentifiedObject object) throws FactoryException { String select = "COORD_REF_SYS_CODE"; String from = "[Coordinate Reference System]"; String where, code; if (object instanceof Ellipsoid) { select = "ELLIPSOID_CODE"; from = "[Ellipsoid]"; where = "SEMI_MAJOR_AXIS"; code = Double.toString(((Ellipsoid) object).getSemiMajorAxis()); } else { IdentifiedObject dependency; if (object instanceof GeneralDerivedCRS) { dependency = ((GeneralDerivedCRS) object).getBaseCRS(); where = "SOURCE_GEOGCRS_CODE"; } else if (object instanceof SingleCRS) { dependency = ((SingleCRS) object).getDatum(); where = "DATUM_CODE"; } else if (object instanceof GeodeticDatum) { dependency = ((GeodeticDatum) object).getEllipsoid(); select = "DATUM_CODE"; from = "[Datum]"; where = "ELLIPSOID_CODE"; } else { return super.getCodeCandidates(object); } dependency = getIdentifiedObjectFinder(dependency.getClass()).find(dependency); Identifier id = AbstractIdentifiedObject.getIdentifier(dependency, getAuthority()); if (id == null || (code = id.getCode()) == null) { return super.getCodeCandidates(object); } } String sql = "SELECT " + select + " FROM " + from + " WHERE " + where + "='" + code + '\''; sql = adaptSQL(sql); final Set<String> result = new LinkedHashSet<String>(); try { final Statement s = getConnection().createStatement(); final ResultSet r = s.executeQuery(sql); while (r.next()) { result.add(r.getString(1)); } r.close(); s.close(); } catch (SQLException exception) { throw databaseFailure(Identifier.class, code, exception); } return result; } } /** * Constructs an exception for recursive calls. */ private static FactoryException recursiveCall(final Class type, final String code) { return new FactoryException(Errors.format(ErrorKeys.RECURSIVE_CALL_$2, type, code)); } /** * Constructs an exception for a database failure. */ private static FactoryException databaseFailure(final Class type, final String code, final SQLException cause) { return new FactoryException(Errors.format(ErrorKeys.DATABASE_FAILURE_$2, type, code), cause); } /** * Invoked when a new {@link PreparedStatement} is about to be created from a SQL string. * Since the <A HREF="http://www.epsg.org">EPSG database</A> is available mainly in MS-Access * format, SQL statements are formatted using some syntax specific to this particular database * software (for example "<code>SELECT * FROM [Coordinate Reference System]</code>"). When * prociding subclass targeting another database vendor, then this method should be overridden * in order to adapt the local SQL syntax. * <p> * For example a subclass connecting to a <cite>PostgreSQL</cite> database could replace * all spaces (" ") between watching braces ("[" and "]") by underscore ("_"). * * @param statement The statement in MS-Access syntax. * @return The SQL statement to use. The default implementation returns the string unchanged. */ protected abstract String adaptSQL(final String statement); /** * Returns {@code true} if the specified code may be a primary key in some table. This method * do not needs to checks any entry in the database. It should just checks from the syntax if * the code looks like a valid EPSG identifier. The default implementation returns {@code true} * if all non-space characters are {@linkplain Character#isDigit(char) digits}. * <p> * When this method returns {@code false}, some {@code createFoo(...)} methods look for the * code in the name column instead of the primary key column. This allows to accept the * "<cite>NTF (Paris) / France I</cite>" string (for example) in addition to the {@code "27581"} * primary key. Both should fetch the same object. * <p> * If this method returns {@code true} in all cases, then this factory never search for matching * names. In such case, an appropriate exception will be thrown in {@code createFoo(...)} * methods if the code is not found in the primary key column. Subclasses can overrides this * method that way if this is the intended behavior. * * @param code The code the inspect. * @return {@code true} if the code is probably a primary key. * @throws FactoryException if an unexpected error occured while inspecting the code. */ protected boolean isPrimaryKey(final String code) throws FactoryException { final int length = code.length(); for (int i=0; i<length; i++) { final char c = code.charAt(i); if (!Character.isDigit(c) && !Character.isSpaceChar(c)) { return false; } } return true; } /** * Returns {@code true} if it is safe to dispose this factory. This method is invoked indirectly * by {@link ThreadedEpsgFactory} after some timeout in order to release resources. This method will * block the disposal if some {@linkplain #getAuthorityCodes set of authority codes} are still * in use. */ final synchronized boolean canDispose() { return true; } /** * Disposes any resources hold by this object. * * @throws FactoryException if an error occurred while closing the connection. */ @Override public synchronized void dispose() throws FactoryException { disconnect(); super.dispose(); } /** * Connect to the database in anticipation of of use. * @throws FactoryException */ public void connect() throws FactoryException { try { getConnection(); } catch (SQLException e) { throw new FactoryException(e); } } /** * Disconnect from the database, and remain idle. We will still keep our * internal data structures, we are not going to hold onto a database connection * unless we are going to be used. * * @throws FactoryException */ public void disconnect() throws FactoryException { if (connection != null) { final boolean isClosed; try { isClosed = connection.isClosed(); for (final Iterator it=statements.values().iterator(); it.hasNext();) { ((PreparedStatement) it.next()).close(); it.remove(); } connection.close(); } catch (SQLException exception) { throw new FactoryException(exception); } if (!isClosed) { /* * The above code was run unconditionally as a safety, even if the connection * was already closed. However we will log a message only if we actually closed * the connection, otherwise the log records are a little bit misleading. */ final LogRecord record = Loggings.format(Level.FINE, LoggingKeys.CLOSED_EPSG_DATABASE); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } connection = null; } } /** * Access to the connection used by this EpsgFactory. The connection will * be created as needed. * * @return the connection */ protected synchronized Connection getConnection() throws SQLException { if (connection == null) { connection = dataSource.getConnection(); } return connection; } /** * Shutdown the database engine. This method is invoked twice by {@link ThreadedEpsgFactory} * at JVM shutdown: one time before the {@linkplain #connection} is closed, and a second * time after. This shutdown hook is useful for <cite>embedded</cite> database engine * starting a server process in addition to the client process. Just closing the connection * is not enough for them. Example: * <P> * <UL> * <LI>HSQL database engine needs to execute a {@code "SHUTDOWN"} statement using the * {@linkplain #connection} before it is closed.</LI> * <LI>Derby database engine needs to instruct the {@linkplain java.sql.DriverManager driver * manager} after all connections have been closed.</LI> * </UL> * <P> * The default implementation does nothing, which is sufficient for implementations * connecting to a distant server (i.e. non-embedded database engine), for example * {@linkplain AccessDataSource MS-Access} or {@linkplain PostgreDataSource PostgreSQL}. * * @param active {@code true} if the {@linkplain #connection} is alive, or {@code false} * otherwise. This method is invoked first with {@code active} set to {@code true}, * then a second time with {@code active} set to {@code false}. * @throws SQLException if this method failed to shutdown the database engine. */ protected void shutdown(final boolean active) throws SQLException { } /** * Invokes {@link #dispose} when this factory is garbage collected. * * @throws Throwable if an error occurred while closing the connection. */ @Override protected final void finalize() throws Throwable { dispose(); super.finalize(); } ////////////////////////////////////////////////////////////////////////////////////////////// ////// /////// ////// HARD CODED VALUES (other than SQL statements) RELATIVE TO THE EPSG DATABASE /////// ////// /////// ////////////////////////////////////////////////////////////////////////////////////////////// /** * Returns a hard-coded unit from an EPSG code. We do not need to provide all units here, * but we must at least provide all base units declared in the [TARGET_UOM_CODE] column * of table [Unit of Measure]. Other units will be derived automatically if they are not * listed here. * * @param code The code. * @return The unit, or {@code null} if the code is unrecognized. */ private static Unit<?> getUnit(final int code) { switch (code) { case 9001: return SI.METER; case 9002: return NonSI.FOOT; case 9030: return NonSI.NAUTICAL_MILE; case 9036: return SI.KILO(SI.METER); case 9101: return SI.RADIAN; case 9122: // Fall through case 9102: return NonSI.DEGREE_ANGLE; case 9103: return NonSI.MINUTE_ANGLE; case 9104: return NonSI.SECOND_ANGLE; case 9105: return NonSI.GRADE; case 9107: return Units.DEGREE_MINUTE_SECOND; case 9108: return Units.DEGREE_MINUTE_SECOND; case 9109: return SI.MICRO(SI.RADIAN); case 9110: return Units.SEXAGESIMAL_DMS; //TODO case 9111: return NonSI.SEXAGESIMAL_DM; case 9203: // Fall through case 9201: return Unit.ONE; case 9202: return Units.PPM; default : return null; } } /** * Set a Bursa-Wolf parameter from an EPSG parameter. * * @param parameters The Bursa-Wolf parameters to modify. * @param code The EPSG code for a parameter from [PARAMETER_CODE] column. * @param value The value of the parameter from [PARAMETER_VALUE] column. * @param unit The unit of the parameter value from [UOM_CODE] column. * @throws FactoryException if the code is unrecognized. */ private static void setBursaWolfParameter(final BursaWolfParameters parameters, final int code, double value, final Unit<?> unit) throws FactoryException { Unit<?> target = unit; if (code >= 8605) { if (code <= 8607) target = SI .METER; else if (code <= 8710) target = NonSI.SECOND_ANGLE; else if (code == 8611) target = Units.PPM; } if (target != unit) { value = unit.getConverterTo(target).convert(value); } switch (code) { case 8605: parameters.dx = value; break; case 8606: parameters.dy = value; break; case 8607: parameters.dz = value; break; case 8608: parameters.ex = value; break; case 8609: parameters.ey = value; break; case 8610: parameters.ez = value; break; case 8611: parameters.ppm = value; break; default: throw new FactoryException(Errors.format(ErrorKeys.UNEXPECTED_PARAMETER_$1, code)); } } /** * List of tables and columns to test for codes values. * This table is used by the {@link #createObject} method in order to detect * which of the following methods should be invoked for a given code: * * {@link #createCoordinateReferenceSystem} * {@link #createCoordinateSystem} * {@link #createDatum} * {@link #createEllipsoid} * {@link #createUnit} * * The order is significant: it is the key for a {@code switch} statement. * * @see #createObject * @see #lastObjectType */ private static final TableInfo[] TABLES_INFO = { new TableInfo(CoordinateReferenceSystem.class, "[Coordinate Reference System]", "COORD_REF_SYS_CODE", "COORD_REF_SYS_NAME", "COORD_REF_SYS_KIND", new Class[] { ProjectedCRS.class, GeographicCRS.class, GeocentricCRS.class}, new String[] {"projected", "geographic", "geocentric"}), new TableInfo(CoordinateSystem.class, "[Coordinate System]", "COORD_SYS_CODE", "COORD_SYS_NAME", "COORD_SYS_TYPE", new Class[] { CartesianCS.class, EllipsoidalCS.class, SphericalCS.class, VerticalCS.class}, new String[] {"Cartesian", "ellipsoidal", "spherical", "vertical"}), new TableInfo(CoordinateSystemAxis.class, "[Coordinate Axis] AS CA INNER JOIN [Coordinate Axis Name] AS CAN"+ " ON CA.COORD_AXIS_NAME_CODE=CAN.COORD_AXIS_NAME_CODE", "COORD_AXIS_CODE", "COORD_AXIS_NAME"), new TableInfo(Datum.class, "[Datum]", "DATUM_CODE", "DATUM_NAME", "DATUM_TYPE", new Class[] { GeodeticDatum.class, VerticalDatum.class, EngineeringDatum.class}, new String[]{"geodetic", "vertical", "engineering"}), new TableInfo(Ellipsoid.class, "[Ellipsoid]", "ELLIPSOID_CODE", "ELLIPSOID_NAME"), new TableInfo(PrimeMeridian.class, "[Prime Meridian]", "PRIME_MERIDIAN_CODE", "PRIME_MERIDIAN_NAME"), new TableInfo(CoordinateOperation.class, "[Coordinate_Operation]", "COORD_OP_CODE", "COORD_OP_NAME", "COORD_OP_TYPE", new Class[] { Projection.class, Conversion.class, Transformation.class}, new String[]{"conversion", "conversion", "transformation"}), // Note: Projection is handle in a special way. new TableInfo(OperationMethod.class, "[Coordinate_Operation Method]", "COORD_OP_METHOD_CODE", "COORD_OP_METHOD_NAME"), new TableInfo(ParameterDescriptor.class, "[Coordinate_Operation Parameter]", "PARAMETER_CODE", "PARAMETER_NAME"), new TableInfo(Unit.class, "[Unit of Measure]", "UOM_CODE", "UNIT_OF_MEAS_NAME") }; /////////////////////////////////////////////////////////////////////////////// //////// //////// //////// E N D O F H A R D C O D E D V A L U E S //////// //////// //////// //////// NOTE: 'createFoo(...)' methods may still have hard-coded //////// //////// values (others than SQL statements) in 'equalsIgnoreCase' //////// //////// expressions. //////// /////////////////////////////////////////////////////////////////////////////// /** * A set of EPSG authority codes. This set makes use of our connection to the EPSG database. * All {@link #iterator} method call creates a new {@link ResultSet} holding the codes. However, * call to {@link #contains} map directly to a SQL call. * <p> * Serialization of this class store a copy of all authority codes. The serialization * do not preserve any connection to the database. * * @since 2.2 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ final class AuthorityCodeSet extends AbstractSet<String> implements Serializable { /** * For compatibility with different versions. */ private static final long serialVersionUID = 7105664579449680562L; /** * The type for this code set. This is translated to the most appropriate * interface type even if the user supplied an implementation type. */ public final Class<?> type; /** * {@code true} if {@link #type} is assignable to {@link Projection}. */ private final boolean isProjection; /** * A view of this set as a map with object's name as values, or {@code null} if none. * Will be created only when first needed. */ private transient java.util.Map<String,String> asMap; /** * The SQL command to use for creating the {@code queryAll} statement. * Used for iteration over all codes. */ final String sqlAll; /** * The SQL command to use for creating the {@code querySingle} statement. * Used for fetching the description from a code. */ private final String sqlSingle; /** * The statement to use for querying all codes. * Will be created only when first needed. */ private transient PreparedStatement queryAll; /** * The statement to use for querying a single code. * Will be created only when first needed. */ private transient PreparedStatement querySingle; /** * The collection's size, or a negative value if not yet computed. The records will be counted * only when first needed. The special value -2 if set by {@link #isEmpty} if the size has not * yet been computed, but we know that the set is not empty. */ private int size = -1; /** * Creates a new set of authority codes for the specified type. * * @param connection The connection to the EPSG database. * @param table The table to query. * @param type The type to query. * @param factory The factory originator. */ public AuthorityCodeSet(final TableInfo table,final Class type){ final StringBuilder buffer = new StringBuilder("SELECT "); buffer.append(table.codeColumn); if (table.nameColumn != null) { buffer.append(", ").append(table.nameColumn); } buffer.append(" FROM ").append(table.table); boolean hasWhere = false; Class tableType = table.type; if (table.typeColumn != null) { for (int i=0; i<table.subTypes.length; i++) { final Class<?> candidate = table.subTypes[i]; if (candidate.isAssignableFrom(type)) { buffer.append(" WHERE (").append(table.typeColumn) .append(" LIKE '").append(table.typeNames[i]).append("%'"); hasWhere = true; tableType = candidate; break; } } if (hasWhere) { buffer.append(')'); } } this.type = tableType; isProjection = Projection.class.isAssignableFrom(tableType); final int length = buffer.length(); buffer.append(" ORDER BY ").append(table.codeColumn); sqlAll = adaptSQL(buffer.toString()); buffer.setLength(length); buffer.append(hasWhere ? " AND " : " WHERE ").append(table.codeColumn).append(" = ?"); sqlSingle = adaptSQL(buffer.toString()); } /** * Returns all codes. */ private ResultSet getAll() throws SQLException { assert Thread.holdsLock(this); if (queryAll != null) { try { return queryAll.executeQuery(); } catch (SQLException ignore) { /* * Failed to reuse an existing statement. This problem occurs in some occasions * with the JDBC-ODBC bridge in Java 6 (the error message is "Invalid handle"). * I'm not sure where the bug come from (didn't noticed it when using HSQL). We * will try again with a new statement created in the code after this 'catch' * clause. Note that we set 'queryAll' to null first in case of failure during * the 'prepareStatement(...)' execution. */ queryAll.close(); queryAll = null; recoverableException("getAll", ignore); } } queryAll = getConnection().prepareStatement(sqlAll); return queryAll.executeQuery(); } /** * Returns a single code. */ private ResultSet getSingle(final Object code) throws SQLException { assert Thread.holdsLock(this); if (querySingle == null) { querySingle = getConnection().prepareStatement(sqlSingle); } querySingle.setString(1, code.toString()); return querySingle.executeQuery(); } /** * Returns {@code true} if the code in the specified result set is acceptable. * This method handle projections in a special way. */ private boolean isAcceptable(final ResultSet results) throws SQLException { if (!isProjection) { return true; } final String code = results.getString(1); return isProjection(code); } /** * Returns {@code true} if the code in the specified code is acceptable. * This method handle projections in a special way. */ private boolean isAcceptable(final String code) throws SQLException { if (!isProjection) { return true; } return isProjection(code); } /** * Returns {@code true} if this collection contains no elements. * This method fetch at most one row instead of counting all rows. */ @Override public synchronized boolean isEmpty() { if (size != -1) { return size == 0; } boolean empty = true; try { final ResultSet results = getAll(); while (results.next()) { if (isAcceptable(results)) { empty = false; break; } } results.close(); } catch (SQLException exception) { unexpectedException("isEmpty", exception); } size = empty ? 0 : -2; return empty; } /** * Count the number of elements in the underlying result set. */ public synchronized int size() { if (size >= 0) { return size; } int count = 0; try { final ResultSet results = getAll(); while (results.next()) { if (isAcceptable(results)) { count++; } } results.close(); } catch (SQLException exception) { unexpectedException("size", exception); } size = count; // Stores only on success. return count; } /** * Returns {@code true} if this collection contains the specified element. */ @Override public synchronized boolean contains(final Object code) { boolean exists = false; if (code != null) try { final ResultSet results = getSingle(code); while (results.next()) { if (isAcceptable(results)) { exists = true; break; } } results.close(); } catch (SQLException exception) { unexpectedException("contains", exception); } return exists; } /** * Returns an iterator over the codes. The iterator is backed by a living {@link ResultSet}, * which will be closed as soon as the iterator reach the last element. */ public synchronized java.util.Iterator<String> iterator() { try { final Iterator iterator = new Iterator(getAll()); /* * Set the statement to null without closing it, in order to force a new statement * creation if getAll() is invoked before the iterator finish its iteration. This * is needed because only one ResultSet is allowed for each Statement. */ queryAll = null; return iterator; } catch (SQLException exception) { unexpectedException("iterator", exception); final Set<String> empty = Collections.emptySet(); return empty.iterator(); } } /** * Returns a serializable copy of this set. This method is invoked automatically during * serialization. The serialised set of authority code is disconnected from the underlying * database. */ protected Object writeReplace() throws ObjectStreamException { return new LinkedHashSet<String>(this); } /** * Closes the underlying statements. Note: this method is also invoked directly * by {@link DirectEpsgFactory#dispose}, which is okay in this particular case since * the implementation of this method can be executed an arbitrary amount of times. */ @Override protected synchronized void finalize() throws SQLException { if (querySingle != null) { querySingle.close(); querySingle = null; } if (queryAll != null) { queryAll.close(); queryAll = null; } } /** * Invoked when an exception occured. This method just log a warning. */ private void unexpectedException(final String method, final SQLException exception) { unexpectedException(AuthorityCodes.class, method, exception); } /** * Invoked when an exception occured. This method just log a warning. */ void unexpectedException(final Class classe, final String method, final SQLException exception) { Logging.unexpectedException(LOGGER, classe, method, exception); } /** * Invoked when a recoverable exception occured. */ private void recoverableException(final String method, final SQLException exception) { // Uses the FINE level instead of WARNING because it may be a recoverable error. LogRecord record = Loggings.format(Level.FINE, LoggingKeys.UNEXPECTED_EXCEPTION); record.setSourceClassName(AuthorityCodes.class.getName()); record.setSourceMethodName(method); record.setThrown(exception); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } /** * The iterator over the codes. This inner class must kept a reference toward the enclosing * {@link AuthorityCodes} in order to prevent a call to {@link AuthorityCodes#finalize} * before the iteration is finished. */ private final class Iterator implements java.util.Iterator<String> { /** The result set, or {@code null} if there is no more elements. */ private ResultSet results; /** The next code. */ private transient String next; /** Creates a new iterator for the specified result set. */ Iterator(final ResultSet results) throws SQLException { assert Thread.holdsLock(AuthorityCodeSet.this); this.results = results; toNext(); } /** Moves to the next element. */ private void toNext() throws SQLException { while (results.next()) { next = results.getString(1); if (isAcceptable(next)) { return; } } finalize(); } /** Returns {@code true} if there is more elements. */ public boolean hasNext() { return results != null; } /** Returns the next element. */ public String next() { if (results == null) { throw new NoSuchElementException(); } final String current = next; try { toNext(); } catch (SQLException exception) { results = null; unexpectedException(Iterator.class, "next", exception); } return current; } /** Always throws an exception, since this iterator is read-only. */ public void remove() { throw new UnsupportedOperationException(); } /** Closes the underlying result set. */ @Override protected void finalize() throws SQLException { next = null; if (results != null) { final PreparedStatement owner = (PreparedStatement) results.getStatement(); results.close(); results = null; synchronized (AuthorityCodeSet.this) { /* * We don't need the statement anymore. Gives it back to the enclosing class * in order to avoid creating a new one when AuthorityCodes.getAll() will be * invoked again, or closes the statement if getAll() already created a new * statement anyway. */ assert owner != queryAll; if (queryAll == null) { queryAll = owner; } else { owner.close(); } } } } } /** * Returns a view of this set as a map with object's name as value, or {@code null} if none. */ final java.util.Map<String,String> asMap() { if (asMap == null) { asMap = new Map(); } return asMap; } /** * A view of {@link AuthorityCodes} as a map, with authority codes as key and * object names as values. */ private final class Map extends AbstractMap<String,String> { /** * Returns the number of key-value mappings in this map. */ @Override public int size() { return AuthorityCodeSet.this.size(); } /** * Returns {@code true} if this map contains no key-value mappings. */ @Override public boolean isEmpty() { return AuthorityCodeSet.this.isEmpty(); } /** * Returns the description to which this map maps the specified EPSG code. */ @Override public String get(final Object code) { String value = null; if (code != null) try { synchronized (AuthorityCodeSet.this) { final ResultSet results = getSingle(code); while (results.next()) { if (isAcceptable(results)) { value = results.getString(2); break; } } results.close(); } } catch (SQLException exception) { unexpectedException("get", exception); } return value; } /** * Returns {@code true} if this map contains a mapping for the specified EPSG code. */ @Override public boolean containsKey(final Object key) { return contains(key); } /** * Returns a set view of the keys contained in this map. */ @Override public Set<String> keySet() { return AuthorityCodeSet.this; } /** * Returns a set view of the mappings contained in this map. * * @todo Not yet implemented. */ public Set<Map.Entry<String,String>> entrySet() { throw new UnsupportedOperationException(); } } } }