blob: c2e730c4df2f15edd047ed65cd0c14663ade169c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.factory.sql;
import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import javax.measure.format.ParserException;
import org.opengis.util.NameSpace;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.citation.OnLineFunction;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.apache.sis.internal.metadata.TransformationAccuracy;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.metadata.sql.SQLUtilities;
import org.apache.sis.internal.referencing.CoordinateOperations;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.referencing.DeferredCoordinateOperation;
import org.apache.sis.internal.referencing.DeprecatedCode;
import org.apache.sis.internal.referencing.EPSGParameterDomain;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.internal.referencing.SignReversalComment;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.ServicesForMetadata;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.system.Semaphores;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.metadata.iso.citation.DefaultCitation;
import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.parameter.DefaultParameterDescriptor;
import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.datum.BursaWolfParameters;
import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
import org.apache.sis.referencing.operation.DefaultOperationMethod;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
import org.apache.sis.referencing.factory.FactoryDataException;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Version;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
import static org.apache.sis.internal.util.StandardDateFormat.UTC;
import static org.apache.sis.internal.referencing.ServicesForMetadata.CONNECTION;
// Branch-dependent imports
import org.apache.sis.internal.util.StandardDateFormat;
import org.apache.sis.referencing.cs.DefaultParametricCS;
import org.apache.sis.referencing.datum.DefaultParametricDatum;
/**
* <cite>Data Access Object</cite> (DAO) creating geodetic objects from a JDBC connection to an EPSG database.
* 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.
*
* <div class="section">Object identifier (code or name)</div>
* EPSG codes are numerical identifiers. For example code 3395 stands for <cite>"WGS 84 / World Mercator"</cite>.
* Coordinate Reference Objects are normally created from their numerical codes, but this factory accepts also names.
* For example {@code createProjectedCRS("3395")} and {@code createProjectedCRS("WGS 84 / World Mercator")} both fetch
* 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 <cite>"WGS 84"</cite> for instance.
* If such an ambiguity is found, an exception will be thrown.
*
* <div class="section">Life cycle and caching</div>
* {@code EPSGDataAccess} instances should be short-lived since they may hold a significant amount of JDBC resources.
* {@code EPSGDataAccess} instances are created on the fly by {@link EPSGFactory} and closed after a relatively short
* {@linkplain EPSGFactory#getTimeout timeout}.
* In addition {@code EPSGFactory} caches the most recently created objects, which reduce greatly
* the amount of {@code EPSGDataAccess} instantiations (and consequently the amount of database accesses)
* in the common case where only a few EPSG codes are used by an application.
* {@code EPSGDataAccess.createFoo(String)} methods do not cache by themselves and query the database on every invocation.
*
* <div class="section">SQL dialects</div>
* Because the primary distribution format for the EPSG dataset is MS-Access, this class uses SQL statements formatted
* for the MS-Access dialect. For usage with other database software products like PostgreSQL or Derby,
* a {@link SQLTranslator} instance is provided to the constructor.
*
* @author Yann Cézard (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Rueben Schulz (UBC)
* @author Matthias Basler
* @author Andrea Aime (TOPP)
* @author Johann Sorel (Geomatys)
* @version 1.0
*
* @see <a href="http://sis.apache.org/tables/CoordinateReferenceSystems.html">List of authority codes</a>
*
* @since 0.7
* @module
*/
public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAuthorityFactory,
CSAuthorityFactory, DatumAuthorityFactory, CoordinateOperationAuthorityFactory, Localized, AutoCloseable
{
/**
* The vertical datum type, which is fixed to a hard-coded value for all vertical datum for now.
* Note that vertical datum type is no longer part of ISO 19111:2007.
*/
static final VerticalDatumType VERTICAL_DATUM_TYPE = VerticalDatumType.GEOIDAL;
/**
* The deprecated ellipsoidal coordinate systems and their replacements. Those coordinate systems are deprecated
* because they use a unit of measurement which is no longer supported by OGC (for example degree-minute-second).
* Those replacements can be used only if the ellipsoidal CS is used for the base geographic CRS of a derived or
* projected CRS, because the units of measurement of the base CRS do not impact the units of measurements of the
* derived CRS.
*
* <p>We perform those replacements for avoiding a "Unit conversion from “DMS” to “°” is non-linear" exception
* at projected CRS creation time.</p>
*
* @see #replaceDeprecatedCS
*/
@Workaround(library = "EPSG:6401-6420", version = "8.9") // Deprecated in 2002 but still present in 2016.
private static final Map<Integer,Integer> DEPRECATED_CS = deprecatedCS();
static Map<Integer,Integer> deprecatedCS() {
final Map<Integer,Integer> m = new HashMap<>(24);
// Ellipsoidal 2D CS. Axes: latitude, longitude. Orientations: north, east. UoM: degree
Integer replacement = 6422;
m.put(6402, replacement);
for (int code = 6405; code <= 6412; code++) {
m.put(code, replacement);
}
// Ellipsoidal 3D CS. Axes: latitude, longitude, ellipsoidal height. Orientations: north, east, up. UoM: degree, degree, metre.
replacement = 6423;
m.put(6401, replacement);
for (int code = 6413; code <= 6420; code++) {
m.put(code, replacement);
}
return m;
}
/**
* The namespace of EPSG names and codes. This namespace is needed by all {@code createFoo(String)} methods.
* The {@code EPSGDataAccess} constructor relies on the {@link EPSGFactory#nameFactory} caching mechanism
* for giving us the same {@code NameSpace} instance than the one used by previous {@code EPSGDataAccess}
* instances, if any.
*/
private final NameSpace namespace;
/**
* The last table in which object name were looked for.
* This is for internal use by {@link #toPrimaryKeys} only.
*/
private String lastTableForName;
/**
* The calendar instance for creating {@link Date} objects from a year (the "epoch" in datum definition).
* We use the UTC timezone, which may not be quite accurate. But there is no obvious timezone for "epoch",
* and the "epoch" is an approximation anyway.
*
* @see #getCalendar()
*/
private Calendar calendar;
/**
* The object to use for parsing dates, created when first needed. This is used for
* parsing the origin of temporal datum. This is an Apache SIS specific extension.
*/
private DateFormat dateFormat;
/**
* A pool of prepared statements. Keys are {@link String} objects related to their originating method
* (for example "Ellipsoid" for {@link #createEllipsoid(String)}).
*/
private final Map<String,PreparedStatement> statements = new HashMap<>();
/**
* The set of authority codes for different types. This map is used by the {@link #getAuthorityCodes(Class)}
* method as a cache for returning the set created in a previous call. We do not want this map to exist for
* a long time anyway.
*
* <p>Note that this {@code EPSGDataAccess} instance can not be closed as long as this map is not empty, since
* {@link AuthorityCodes} caches some SQL statements and consequently require the {@linkplain #connection}
* to be open. This is why we use weak references rather than hard ones, in order to know when no
* {@link AuthorityCodes} are still in use.</p>
*
* <p>The {@link CloseableReference#dispose()} method takes care of closing the statements used by the map.
* The {@link AuthorityCodes} reference in this map is then cleared by the garbage collector.
* The {@link #canClose()} method checks if there is any remaining live reference in this map,
* and returns {@code false} if some are found (thus blocking the call to {@link #close()}
* by the {@link org.apache.sis.referencing.factory.ConcurrentAuthorityFactory} timer).</p>
*/
private final Map<Class<?>, CloseableReference<AuthorityCodes>> authorityCodes = new HashMap<>();
/**
* Cache for axis names. This service is not provided by {@code ConcurrentAuthorityFactory}
* since {@link AxisName} objects are particular to the EPSG database.
*
* @see #getAxisName(Integer)
*/
private final Map<Integer,AxisName> axisNames = new HashMap<>();
/**
* Cache for the number of dimensions of coordinate systems. This service is not provided by
* {@code ConcurrentAuthorityFactory} since the number of dimension is used internally in this class.
*
* @see #getDimensionForCS(Integer)
*/
private final Map<Integer,Integer> csDimensions = new HashMap<>();
/**
* Cache for whether conversions are projections. This service is not provided by {@code ConcurrentAuthorityFactory}
* since the check for conversion type is used internally in this class.
*
* @see #isProjection(Integer)
*/
private final Map<Integer,Boolean> isProjection = new HashMap<>();
/**
* Cache of naming systems other than EPSG. There is usually few of them (at most 15).
* This is used for aliases.
*
* @see #createProperties(String, String, Integer, CharSequence, boolean)
*/
private final Map<String,NameSpace> namingSystems = new HashMap<>();
/**
* The properties to be given the objects to construct.
* Reused every time {@code createProperties(…)} is invoked.
*/
private final Map<String,Object> properties = new HashMap<>();
/**
* A safety guard for preventing never-ending loops in recursive calls to some {@code createFoo(String)} methods.
* Recursivity may happen while creating Bursa-Wolf parameters, projected CRS if the database has erroneous data,
* compound CRS if there is cycles, or coordinate operations.
*
* <div class="note"><b>Example:</b>
* {@link #createDatum(String)} invokes {@link #createBursaWolfParameters(PrimeMeridian, Integer)}, which creates
* a target datum. The target datum could have its own Bursa-Wolf parameters, with one of them pointing again to
* the source datum.</div>
*
* Keys are EPSG codes and values are the type of object being constructed (but those values are not yet used).
*/
private final Map<Integer,Class<?>> safetyGuard = new HashMap<>();
/**
* {@code true} for disabling the logging of warnings when this factory creates deprecated objects.
* This flag should be always {@code false}, except during {@link EPSGCodeFinder#find(IdentifiedObject)}
* execution since that method may temporarily creates deprecated objects which are later discarded.
* May also be {@code false} when creating base CRS of deprecated projected or derived CRS.
*/
transient boolean quiet;
/**
* {@code true} if {@link #createCoordinateReferenceSystem(String)} is allowed to replace deprecated
* coordinate system at CRS creation time. This flag should be set to {@code true} only when creating
* the base CRS of a projected or derived CRS.
*
* @see #DEPRECATED_CS
*/
private transient boolean replaceDeprecatedCS;
/**
* The {@code ConcurrentAuthorityFactory} that created this Data Access Object (DAO).
* The owner supplies caching for all {@code createFoo(String)} methods.
*/
protected final EPSGFactory owner;
/**
* The connection to the EPSG database. This connection is specified at {@linkplain #EPSGDataAccess construction time}
* and closed by the {@link #close()} method.
*
* @see #close()
*/
protected final Connection connection;
/**
* The translator from the SQL statements using MS-Access dialect
* to SQL statements using the dialect of the actual database.
*/
protected final SQLTranslator translator;
/**
* Creates a factory using the given connection. The connection will be {@linkplain Connection#close() closed}
* when this factory will be {@linkplain #close() closed}.
*
* <div class="note"><b>API design note:</b>
* this constructor is protected because {@code EPSGDataAccess} instances should not be created as standalone factories.
* This constructor is for allowing definition of custom {@code EPSGDataAccess} subclasses, which are then instantiated
* by the {@link EPSGFactory#newDataAccess(Connection, SQLTranslator)} method of a corresponding custom
* {@code EPSGFactory} subclass.</div>
*
* @param owner the {@code EPSGFactory} which is creating this Data Access Object (DAO).
* @param connection the connection to the underlying EPSG database.
* @param translator the translator from the SQL statements using MS-Access dialect
* to SQL statements using the dialect of the actual database.
*
* @see EPSGFactory#newDataAccess(Connection, SQLTranslator)
*/
protected EPSGDataAccess(final EPSGFactory owner, final Connection connection, final SQLTranslator translator) {
ArgumentChecks.ensureNonNull("connection", connection);
ArgumentChecks.ensureNonNull("translator", translator);
this.owner = owner;
this.connection = connection;
this.translator = translator;
this.namespace = owner.nameFactory.createNameSpace(
owner.nameFactory.createLocalName(null, Constants.IOGP), null);
}
/**
* Returns the locale used by this factory for producing error messages.
* This locale does not change the way data are read from the EPSG database.
*
* @return the locale for error messages.
*/
@Override
public Locale getLocale() {
return owner.getLocale();
}
/**
* Returns the calendar to use for reading dates in the database.
*/
@SuppressWarnings("ReturnOfDateField")
private Calendar getCalendar() {
if (calendar == null) {
calendar = Calendar.getInstance(TimeZone.getTimeZone(UTC), Locale.CANADA);
// Canada locale is closer to ISO than US.
}
calendar.clear();
return calendar;
}
/**
* Returns the authority for this EPSG dataset. The returned citation contains the database version
* in the {@linkplain Citation#getEdition() edition} attribute, together with date of last update in
* the {@linkplain Citation#getEditionDate() edition date}.
* Example (the exact content will vary with Apache SIS versions, JDBC driver and EPSG dataset versions):
*
* {@preformat text
* Citation
* ├─ Title ……………………………………………………… EPSG Geodetic Parameter Dataset
* ├─ Identifier ………………………………………… EPSG
* ├─ Online resource (1 of 2)
* │ ├─ Linkage ………………………………………… http://epsg-registry.org/
* │ └─ Function ……………………………………… Browse
* └─ Online resource (2 of 2)
* ├─ Linkage ………………………………………… jdbc:derby:/my/path/to/SIS_DATA/Databases/SpatialMetadata
* ├─ Description ……………………………… EPSG dataset version 9.1 on “Apache Derby Embedded JDBC Driver” version 10.14.
* └─ Function ……………………………………… Connection
* }
*/
@Override
public synchronized Citation getAuthority() {
/*
* We do not cache this citation because the caching service is already provided by ConcurrentAuthorityFactory.
*/
final DefaultCitation c = new DefaultCitation("EPSG Geodetic Parameter Dataset");
c.setIdentifiers(Collections.singleton(new ImmutableIdentifier(null, null, Constants.EPSG)));
try {
/*
* Get the most recent version number from the history table. We get the date in local timezone
* instead then UTC because the date is for information purpose only, and the local timezone is
* more likely to be shown nicely (without artificial hours) to the user.
*/
final String query = translator.apply("SELECT VERSION_NUMBER, VERSION_DATE FROM [Version History]" +
" ORDER BY VERSION_DATE DESC, VERSION_HISTORY_CODE DESC");
String version = null;
try (Statement stmt = connection.createStatement();
ResultSet result = stmt.executeQuery(query))
{
while (result.next()) {
version = getOptionalString(result, 1);
final Date date = result.getDate(2); // Local timezone.
if (version != null && date != null) { // Paranoiac check.
c.setEdition(new SimpleInternationalString(version));
c.setEditionDate(date);
break;
}
}
}
/*
* Add some hard-coded links to EPSG resources, and finally add the JDBC driver name and version number.
* The list last OnlineResource looks like:
*
* Linkage: jdbc:derby:/my/path/to/SIS_DATA/Databases/SpatialMetadata
* Function: Connection
* Description: EPSG dataset version 9.1 on “Apache Derby Embedded JDBC Driver” version 10.14.
*
* TODO: A future version should use Citations.EPSG as a template.
*/
final DatabaseMetaData metadata = connection.getMetaData();
addURIs: for (int i=0; ; i++) {
String url;
OnLineFunction function;
InternationalString description = null;
switch (i) {
case 0: url = "http://epsg-registry.org/"; function = OnLineFunction.SEARCH; break;
case 1: url = "http://www.epsg.org/"; function = OnLineFunction.DOWNLOAD; break;
case 2: {
url = SQLUtilities.getSimplifiedURL(metadata);
function = OnLineFunction.valueOf(CONNECTION);
description = Resources.formatInternational(Resources.Keys.GeodeticDataBase_4,
Constants.EPSG, version, metadata.getDatabaseProductName(),
Version.valueOf(metadata.getDatabaseMajorVersion(),
metadata.getDatabaseMinorVersion()));
break;
}
default: break addURIs; // Finished adding all URIs.
}
final DefaultOnlineResource r = new DefaultOnlineResource();
try {
r.setLinkage(new URI(url));
} catch (URISyntaxException exception) {
unexpectedException("getAuthority", exception);
}
r.setFunction(function);
r.setDescription(description);
c.getOnlineResources().add(r);
}
} catch (SQLException exception) {
unexpectedException("getAuthority", exception);
} finally {
c.transitionTo(DefaultCitation.State.FINAL);
}
return c;
}
/**
* Returns the set of authority codes of the given type.
* This returned set may keep a connection to the EPSG database,
* so the set can execute efficiently idioms like the following one:
*
* {@preformat java
* getAuthorityCodes(type).containsAll(others)
* }
*
* The returned set should not be referenced for a long time, as it may prevent this factory to release
* JDBC resources. If the set of codes is needed for a long time, their values should be copied in another
* collection object.
*
* <div class="section">Handling of deprecated objects</div>
* The collection returned by this method gives an enumeration of EPSG codes for valid objects only.
* The EPSG codes of deprecated objects are not included in iterations, computation of {@code Set.size()} value,
* {@code Set.toString()} result, <i>etc.</i> with one exception:
* a call to {@code Set.contains(…)} will return {@code true} if the given identifier exists
* for a deprecated object, even if that identifier does not show up in iterations.
* In other words, the returned collection behaves as if deprecated codes were included in the set but invisible.
*
* @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 (may be an empty set).
* @throws FactoryException if access to the underlying database failed.
*/
@Override
public Set<String> getAuthorityCodes(final Class<? extends IdentifiedObject> type) throws FactoryException {
try {
return getCodeMap(type).keySet();
} catch (SQLException exception) {
throw new FactoryException(exception.getLocalizedMessage(), exception);
}
}
/**
* Returns a map of EPSG authority codes as keys and object names as values.
*/
private synchronized Map<String,String> getCodeMap(final Class<?> type) throws SQLException {
CloseableReference<AuthorityCodes> reference = authorityCodes.get(type);
if (reference != null) {
AuthorityCodes existing = reference.get();
if (existing != null) {
return existing;
}
}
Map<String,String> result = Collections.emptyMap();
for (final TableInfo table : TableInfo.EPSG) {
/*
* We test 'isAssignableFrom' in the two ways for catching the following use cases:
*
* - table.type.isAssignableFrom(type)
* is for the case where a table is for CoordinateReferenceSystem while the user type is some subtype
* like GeographicCRS. The GeographicCRS need to be queried into the CoordinateReferenceSystem table.
* An additional filter will be applied inside the AuthorityCodes class implementation.
*
* - type.isAssignableFrom(table.type)
* is for the case where the user type is IdentifiedObject or Object, in which case we basically want
* to iterate through every tables.
*/
if (table.type.isAssignableFrom(type) || type.isAssignableFrom(table.type)) {
/*
* Maybe an instance already existed but was not found above because the user specified some
* implementation class instead of an interface class. Before to return a newly created map,
* check again in the cached maps using the type computed by AuthorityCodes itself.
*/
AuthorityCodes codes = new AuthorityCodes(connection, table, type, this);
reference = authorityCodes.get(codes.type);
if (reference != null) {
AuthorityCodes existing = reference.get();
if (existing != null) {
codes = existing;
} else {
reference = null; // The weak reference is no longer valid.
}
}
if (reference == null) {
reference = codes.createReference();
authorityCodes.put(codes.type, reference);
}
if (type != codes.type) {
authorityCodes.put(type, reference);
}
/*
* We now have the codes for a single type. Append with the codes of previous types, if any.
* This usually happen only if the user asked for the IdentifiedObject type. Of course this
* break all our effort to query the data only when first needed, but the user should ask
* for more specific types.
*/
if (result.isEmpty()) {
result = codes;
} else {
if (result instanceof AuthorityCodes) {
result = new LinkedHashMap<>(result);
}
result.putAll(codes);
}
}
}
return result;
}
/**
* Returns an empty set since this data access class expects no namespace.
* Code shall be given to {@code createFoo(String)} methods directly, without {@code "EPSG:"} prefix.
*
* @return empty set.
*/
@Override
public Set<String> getCodeSpaces() {
return Collections.emptySet();
}
/**
* Gets a description of the object corresponding to a code.
* This method returns the object name in a lightweight manner, without creating the full {@link IdentifiedObject}.
*
* @param code value allocated by authority.
* @return the object name, or {@code null} if the object corresponding to the specified {@code code} has no name.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the query failed for some other reason.
*/
@Override
public InternationalString getDescriptionText(final String code) throws NoSuchAuthorityCodeException, FactoryException {
try {
for (final TableInfo table : TableInfo.EPSG) {
final String text = getCodeMap(table.type).get(code);
if (text != null) {
return (table.nameColumn != null) ? new SimpleInternationalString(text) : null;
}
}
} catch (SQLException exception) {
throw new FactoryException(exception.getLocalizedMessage(), exception);
}
throw noSuchAuthorityCode(IdentifiedObject.class, code);
}
/**
* Returns {@code true} if the specified code may be a primary key in some table.
* This method does not need to check any entry in the database.
* It should just check from the syntax if the code looks like a valid EPSG identifier.
*
* <p>When this method returns {@code false}, {@code createFoo(String)} methods
* may look for the code in the name column instead than the primary key column.
* This allows to accept the <cite>"WGS 84 / World Mercator"</cite> string (for example)
* in addition to the {@code "3395"} primary key. Both string values should fetch the same object.</p>
*
* <p>If this method returns {@code true}, then this factory does not search for matching names.
* In such case, an appropriate exception will be thrown in {@code createFoo(String)} methods
* if the code is not found in the primary key column.</p>
*
* <div class="section">Default implementation</div>
* The default implementation returns {@code true} if all characters are decimal digits 0 to 9.
*
* @param code the code the inspect.
* @return {@code true} if the code is probably a primary key.
* @throws FactoryException if an unexpected error occurred while inspecting the code.
*/
private boolean isPrimaryKey(final String code) throws FactoryException {
int i = code.length();
if (i == 0) {
return false;
}
do {
final char c = code.charAt(--i);
if (c < '0' || c > '9') {
return false;
}
} while (i != 0);
return true;
}
/**
* Converts EPSG codes or EPSG names to the numerical identifiers (the primary keys).
*
* <div class="note"><b>Note:</b>
* this method could be seen as the converse of above {@link #getDescriptionText(String)} method.</div>
*
* @param table the table where the code should appears, or {@code null} if {@code codeColumn} is null.
* @param codeColumn the column name for the codes, or {@code null} if none.
* @param nameColumn the column name for the names, or {@code null} if none.
* @param codes the codes or names to convert to primary keys, as an array of length 1 or 2.
* @return the numerical identifiers (i.e. the table primary key values).
* @throws SQLException if an error occurred while querying the database.
*/
private int[] toPrimaryKeys(final String table, final String codeColumn, final String nameColumn, final String... codes)
throws SQLException, FactoryException
{
final int[] primaryKeys = new int[codes.length];
codes: for (int i=0; i<codes.length; i++) {
final String code = codes[i];
if (codeColumn != null && nameColumn != null && !isPrimaryKey(code)) {
/*
* The given string is not a numerical code. Search the value in the database.
* We search first in the primary table. If no name is not found there, then we
* will search in the aliases table as a fallback.
*/
final String pattern = toLikePattern(code);
Integer resolved = null;
boolean alias = false;
do {
PreparedStatement stmt;
if (alias) {
// Note: "OBJECT_TABLE_NAME=?" is handled in a special way by SQLTranslator.
stmt = prepareStatement("AliasKey", "SELECT OBJECT_CODE, ALIAS FROM [Alias] WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?");
stmt.setString(1, table);
stmt.setString(2, pattern);
} else {
/*
* The SQL query for searching in the primary table is a little bit more complicated than the query for
* searching in the aliass table. 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 here.
*/
final String KEY = "PrimaryKey";
stmt = statements.get(KEY);
if (stmt != null) {
if (!table.equals(lastTableForName)) {
statements.remove(KEY);
stmt.close();
stmt = null;
lastTableForName = null;
}
}
if (stmt == null) {
stmt = connection.prepareStatement(translator.apply(
"SELECT " + codeColumn + ", " + nameColumn +
" FROM [" + table + "] WHERE " + nameColumn + " LIKE ?"));
statements.put(KEY, stmt);
lastTableForName = table;
}
stmt.setString(1, pattern);
}
try (ResultSet result = stmt.executeQuery()) {
while (result.next()) {
if (SQLUtilities.filterFalsePositive(code, result.getString(2))) {
resolved = ensureSingleton(getOptionalInteger(result, 1), resolved, code);
}
}
}
if (resolved != null) {
primaryKeys[i] = resolved;
continue codes;
}
} while ((alias = !alias) == true);
}
/*
* At this point, 'identifier' should be the primary key. It may still be a non-numerical string
* if the above code did not found a match in the name column or in the alias table.
*/
try {
primaryKeys[i] = Integer.parseInt(code);
} catch (NumberFormatException e) {
throw (NoSuchAuthorityCodeException) new NoSuchAuthorityCodeException(error().getString(
Errors.Keys.IllegalIdentifierForCodespace_2, Constants.EPSG, code), Constants.EPSG, code).initCause(e);
}
}
return primaryKeys;
}
/**
* Creates a statement and executes for the given codes. The first code value is assigned to parameter #1,
* the second code value (if any) is assigned to parameter #2, <i>etc</i>. If a given code is not a
* {@linkplain #isPrimaryKey primary key}, then this method assumes that the code is the object name
* and will search for its primary key value.
*
* @param table the table where the code should appears.
* @param codeColumn the column name for the codes, or {@code null} if none.
* @param nameColumn the column name for the names, or {@code null} if none.
* @param sql the SQL statement to use for creating the {@link PreparedStatement} object.
* Will be used only if no prepared statement was already created for the given code.
* @param codes the codes of the object to create, as an array of length 1 or 2.
* @return the result of the query.
* @throws SQLException if an error occurred while querying the database.
*/
private ResultSet executeQuery(final String table, final String codeColumn, final String nameColumn,
final String sql, final String... codes) throws SQLException, FactoryException
{
assert Thread.holdsLock(this);
assert sql.contains('[' + table + ']') : table;
assert (codeColumn == null) || sql.contains(codeColumn) || table.equals("Area") : codeColumn;
assert (nameColumn == null) || sql.contains(nameColumn) || table.equals("Area") : nameColumn;
return executeQuery(table, sql, toPrimaryKeys(table, codeColumn, nameColumn, codes));
}
/**
* Creates a statement and executes for the given codes. The first code value is assigned to parameter #1,
* the second code value (if any) is assigned to parameter #2, <i>etc</i>.
*
* @param table a key uniquely identifying the caller (e.g. {@code "Ellipsoid"} for {@link #createEllipsoid(String)}).
* @param sql the SQL statement to use for creating the {@link PreparedStatement} object.
* Will be used only if no prepared statement was already created for the specified key.
* @param codes the codes of the object to create, as an array of length 1 or 2.
* @return the result of the query.
* @throws SQLException if an error occurred while querying the database.
*/
private ResultSet executeQuery(final String table, final String sql, final int... codes) throws SQLException {
assert Thread.holdsLock(this);
assert CharSequences.count(sql, '?') == codes.length;
PreparedStatement stmt = prepareStatement(table, sql);
// Partial check that the statement is for the right SQL query.
assert stmt.getParameterMetaData().getParameterCount() == codes.length;
for (int i=0; i<codes.length; i++) {
stmt.setInt(i+1, codes[i]);
}
return stmt.executeQuery();
}
/**
* Executes a query of the form {@code "SELECT … FROM Alias WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?"}.
* The first argument shall be the name of a database table.
*
* @param key a key uniquely identifying the caller.
* @param sql the SQL statement to use for creating the {@link PreparedStatement} object.
* @param table the table to set in the first parameter.
* @param code the object code to set in the second parameter.
* @return the result of the query.
* @throws SQLException if an error occurred while querying the database.
*/
private ResultSet executeMetadataQuery(final String key, final String sql, final String table, final int code) throws SQLException {
assert Thread.holdsLock(this);
assert CharSequences.count(sql, '?') == 2;
PreparedStatement stmt = prepareStatement(key, sql);
assert stmt.getParameterMetaData().getParameterCount() == 2;
stmt.setString(1, table);
stmt.setInt(2, code);
return stmt.executeQuery();
}
/**
* Returns the cached statement or create a new one for the given table.
* The {@code table} argument shall be a key uniquely identifying the caller.
* The {@code sql} argument is used for preparing a new statement if no cached instance exists.
*/
private PreparedStatement prepareStatement(final String table, final String sql) throws SQLException {
PreparedStatement stmt = statements.get(table);
if (stmt == null) {
stmt = connection.prepareStatement(translator.apply(sql));
statements.put(table, stmt);
}
return stmt;
}
/**
* Gets the value from the specified {@link ResultSet}, or {@code null} if none.
*
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the string at the specified column, or {@code null}.
* @throws SQLException if an error occurred while querying the database.
*/
private static String getOptionalString(final ResultSet result, final int columnIndex) throws SQLException {
String value = result.getString(columnIndex);
return (value != null) && !(value = value.trim()).isEmpty() && !result.wasNull() ? value : null;
}
/**
* Gets the value from the specified {@link ResultSet}, or {@code NaN} if none.
*
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the number at the specified column, or {@code NaN}.
* @throws SQLException if an error occurred while querying the database.
*/
private static double getOptionalDouble(final ResultSet result, final int columnIndex) throws SQLException {
final double value = result.getDouble(columnIndex);
return result.wasNull() ? Double.NaN : value;
}
/**
* Gets the value from the specified {@link ResultSet}, or {@code null} if none.
*
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the integer at the specified column, or {@code null}.
* @throws SQLException if an error occurred while querying the database.
*/
private static Integer getOptionalInteger(final ResultSet result, final int columnIndex) throws SQLException {
final int value = result.getInt(columnIndex);
return result.wasNull() ? null : value;
}
/**
* Gets the value from the specified {@link ResultSet}, or {@code false} if none.
* The EPSG database stores boolean values as integers instead than using the SQL type.
*
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the boolean at the specified column, or {@code null}.
* @throws SQLException if an error occurred while querying the database.
*/
private boolean getOptionalBoolean(final ResultSet result, final int columnIndex) throws SQLException {
return translator.useBoolean() ? result.getBoolean(columnIndex) : (result.getInt(columnIndex) != 0);
}
/**
* Formats an error message for an unexpected null value.
*/
private String nullValue(final ResultSet result, final int columnIndex, final Comparable<?> code) throws SQLException {
final ResultSetMetaData metadata = result.getMetaData();
final String column = metadata.getColumnName(columnIndex);
final String table = metadata.getTableName (columnIndex);
result.close();
return error().getString(Errors.Keys.NullValueInTable_3, table, column, code);
}
/**
* Same as {@link #getString(Comparable, ResultSet, int)},
* but reports the fault on an alternative column if the value is null.
*/
private String getString(final String code, final ResultSet result, final int columnIndex, final int columnFault)
throws SQLException, FactoryDataException
{
String value = result.getString(columnIndex);
if (value == null || (value = value.trim()).isEmpty() || result.wasNull()) {
throw new FactoryDataException(nullValue(result, columnFault, code));
}
return value;
}
/**
* Gets the string from the specified {@link ResultSet}.
* The string is required to be non-null. A null string will throw an exception.
*
* @param code the identifier of the record where the string was found.
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the string at the specified column.
* @throws SQLException if an error occurred while querying the database.
* @throws FactoryDataException if a null value was found.
*/
private String getString(final Comparable<?> code, final ResultSet result, final int columnIndex)
throws SQLException, FactoryDataException
{
String value = result.getString(columnIndex);
if (value == null || (value = value.trim()).isEmpty() || result.wasNull()) {
throw new FactoryDataException(nullValue(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 code the identifier of the record where the double was found.
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the double at the specified column.
* @throws SQLException if an error occurred while querying the database.
* @throws FactoryDataException if a null value was found.
*/
private double getDouble(final Comparable<?> code, final ResultSet result, final int columnIndex)
throws SQLException, FactoryDataException
{
final double value = result.getDouble(columnIndex);
if (Double.isNaN(value) || result.wasNull()) {
throw new FactoryDataException(nullValue(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.
*
* <p>We return the value as the {@code Integer} wrapper instead than the {@code int} primitive type
* because the caller will often need that value as an object (for use as key in {@link HashMap}, etc.).</p>
*
* @param code the identifier of the record where the integer was found.
* @param result the result set to fetch value from.
* @param columnIndex the column index (1-based).
* @return the integer at the specified column.
* @throws SQLException if an error occurred while querying the database.
* @throws FactoryDataException if a null value was found.
*/
private Integer getInteger(final Comparable<?> code, final ResultSet result, final int columnIndex)
throws SQLException, FactoryDataException
{
final int value = result.getInt(columnIndex);
if (result.wasNull()) {
throw new FactoryDataException(nullValue(result, columnIndex, code));
}
return value;
}
/**
* Makes sure that an object constructed from the database is not incoherent.
* If the code supplied to a {@code createFoo(String)} 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 content.
*
* @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 FactoryDataException if a duplication has been detected.
*/
private <T> T ensureSingleton(final T newValue, final T oldValue, final Comparable<?> code) throws FactoryDataException {
if (oldValue == null) {
return newValue;
}
if (oldValue.equals(newValue)) {
return oldValue;
}
throw new FactoryDataException(error().getString(Errors.Keys.DuplicatedIdentifier_1, code));
}
/**
* Ensures that this factory is not already building an object of the given code.
* This method shall be followed by a {@code try ... finally} block like below:
*
* {@preformat java
* ensureNoCycle(type, code);
* try {
* ...
* } finally {
* endOfRecursive(type, code);
* }
* }
*/
private void ensureNoCycle(final Class<?> type, final Integer code) throws FactoryException {
if (safetyGuard.putIfAbsent(code, type) != null) {
throw new FactoryException(resources().getString(Resources.Keys.RecursiveCreateCallForCode_2, type, code));
}
}
/**
* Invoked after the block protected against infinite recursivity.
*/
private void endOfRecursivity(final Class<?> type, final Integer code) throws FactoryException {
if (safetyGuard.remove(code) != type) {
throw new FactoryException(String.valueOf(code)); // Would be an EPSGDataAccess bug if it happen.
}
}
/**
* Logs a warning saying that the given code is deprecated and returns the code of the proposed replacement.
*
* @param table the table of the deprecated code.
* @param code the deprecated code.
* @return the proposed replacement (may be the "(none)" text).
*/
private String getSupersession(final String table, final Integer code, final Locale locale) throws SQLException {
String reason = null;
Object replacedBy = null;
try (ResultSet result = executeMetadataQuery("Deprecation",
"SELECT DEPRECATION_REASON, REPLACED_BY FROM [Deprecation]" +
" WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?", table, code))
// Note: "OBJECT_TABLE_NAME=?" is handled in a special way by SQLTranslator.
{
while (result.next()) {
reason = getOptionalString (result, 1);
replacedBy = getOptionalInteger(result, 2);
if (replacedBy != null) break; // Prefer the first record providing a replacement.
}
}
if (replacedBy == null) {
replacedBy = '(' + Vocabulary.getResources(locale).getString(Vocabulary.Keys.None).toLowerCase(locale) + ')';
} else {
replacedBy = replacedBy.toString();
}
/*
* Try to infer the method name from the table name. For example if the deprecated code was found in
* the [Coordinate Reference System] table, then we declare createCoordinateReferenceSystem(String)
* as the source of the log message.
*/
String method = "create";
for (final TableInfo info : TableInfo.EPSG) {
if (TableInfo.tableMatches(info.table, table)) {
method += info.type.getSimpleName();
break;
}
}
if (!quiet) {
LogRecord record = Resources.forLocale(locale).getLogRecord(Level.WARNING, Resources.Keys.DeprecatedCode_3,
Constants.EPSG + Constants.DEFAULT_SEPARATOR + code, replacedBy, reason);
record.setLoggerName(Loggers.CRS_FACTORY);
Logging.log(EPSGDataAccess.class, method, record);
}
return (String) replacedBy;
}
/**
* Returns the name and aliases for the {@link IdentifiedObject} to construct.
*
* @param table the table on which a query has been executed.
* @param name the name for the {@link IdentifiedObject} to construct.
* @param code the EPSG code of the object to construct.
* @param remarks remarks as a {@link String} or {@link InternationalString}, or {@code null} if none.
* @param deprecated {@code true} if the object to create is deprecated.
* @return the name together with a set of properties.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private Map<String,Object> createProperties(final String table, String name, final Integer code,
CharSequence remarks, final boolean deprecated) throws SQLException, FactoryDataException
{
/*
* Search for aliases. Note that searching for the object code is not sufficient. We also need to check if the
* record is really from the table we are looking for since different tables may have objects with the same ID.
*
* Some aliases are identical to the name except that some letters are replaced by their accented letters.
* For example "Reseau Geodesique Francais" → "Réseau Géodésique Français". If we find such alias, replace
* the name by the alias so we have proper display in user interface. Notes:
*
* - WKT formatting will still be compliant with ISO 19162 because the WKT formatter replaces accented
* letters by ASCII ones.
* - We do not perform this replacement directly in our EPSG database because ASCII letters are more
* convenient for implementing accent-insensitive searches.
*/
final List<GenericName> aliases = new ArrayList<>();
try (ResultSet result = executeMetadataQuery("Alias",
"SELECT NAMING_SYSTEM_NAME, ALIAS" +
" FROM [Alias] INNER JOIN [Naming System]" +
" ON [Alias].NAMING_SYSTEM_CODE =" +
" [Naming System].NAMING_SYSTEM_CODE" +
" WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?", table, code))
// Note: "OBJECT_TABLE_NAME=?" is handled in a special way by SQLTranslator.
{
while (result.next()) {
final String naming = getOptionalString(result, 1);
final String alias = getString(code, result, 2);
NameSpace ns = null;
if (naming != null) {
ns = namingSystems.get(naming);
if (ns == null) {
ns = owner.nameFactory.createNameSpace(owner.nameFactory.createLocalName(null, naming), null);
namingSystems.put(naming, ns);
}
}
if (CharSequences.toASCII(alias).toString().equals(name)) {
name = alias;
} else {
aliases.add(owner.nameFactory.createLocalName(ns, alias));
}
}
}
/*
* At this point we can fill the properties map.
*/
properties.clear();
GenericName gn = null;
final Locale locale = getLocale();
final Citation authority = owner.getAuthority();
final InternationalString edition = authority.getEdition();
final String version = (edition != null) ? edition.toString() : null;
if (name != null) {
gn = owner.nameFactory.createGenericName(namespace, Constants.EPSG, name);
properties.put("name", gn);
properties.put(NamedIdentifier.CODE_KEY, name);
properties.put(NamedIdentifier.VERSION_KEY, version);
properties.put(NamedIdentifier.AUTHORITY_KEY, authority);
properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale);
final NamedIdentifier id = new NamedIdentifier(properties);
properties.clear();
properties.put(IdentifiedObject.NAME_KEY, id);
}
if (!aliases.isEmpty()) {
properties.put(IdentifiedObject.ALIAS_KEY, aliases.toArray(new GenericName[aliases.size()]));
}
if (code != null) {
final String codeString = code.toString();
final ImmutableIdentifier identifier;
if (deprecated) {
final String replacedBy = getSupersession(table, code, locale);
identifier = new DeprecatedCode(authority, Constants.EPSG, codeString, version,
Character.isDigit(replacedBy.charAt(0)) ? replacedBy : null,
Vocabulary.formatInternational(Vocabulary.Keys.SupersededBy_1, replacedBy));
properties.put(AbstractIdentifiedObject.DEPRECATED_KEY, Boolean.TRUE);
} else {
identifier = new ImmutableIdentifier(authority, Constants.EPSG, codeString, version,
(gn != null) ? gn.toInternationalString() : null);
}
properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifier);
}
properties.put(IdentifiedObject.REMARKS_KEY, remarks);
properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale);
properties.put(ReferencingFactoryContainer.MT_FACTORY, owner.mtFactory);
return properties;
}
/**
* Returns the name, aliases and domain of validity for the {@link IdentifiedObject} to construct.
*
* @param table the table on which a query has been executed.
* @param name the name for the {@link IdentifiedObject} to construct.
* @param code the EPSG code of the object to construct.
* @param domainCode the code for the domain of validity, or {@code null} if none.
* @param scope the scope, or {@code null} if none.
* @param remarks remarks, or {@code null} if none.
* @param deprecated {@code true} if the object to create is deprecated.
* @return the name together with a set of properties.
*/
private Map<String,Object> createProperties(final String table, final String name, final Integer code,
final String domainCode, String scope, final String remarks, final boolean deprecated)
throws SQLException, FactoryException
{
if ("?".equals(scope)) { // EPSG sometime uses this value for unspecified scope.
scope = null;
}
final Map<String,Object> properties = createProperties(table, name, code, remarks, deprecated);
if (domainCode != null) {
properties.put(Datum.DOMAIN_OF_VALIDITY_KEY, owner.createExtent(domainCode));
}
properties.put(Datum.SCOPE_KEY, scope);
return properties;
}
/**
* Returns a string like the given string but with accented letters replaced by ASCII letters
* and all characters that are not letter or digit replaced by the wildcard % character.
*/
private static String toLikePattern(final String name) {
final StringBuilder buffer = new StringBuilder(name.length());
SQLUtilities.toLikePattern(name, 0, name.length(), false, false, buffer);
return buffer.toString();
}
/**
* Returns an arbitrary object from a code. The default implementation delegates to more specific methods,
* for example {@link #createCoordinateReferenceSystem(String)}, {@link #createDatum(String)}, <i>etc.</i>
* until a successful one is found.
*
* <p><strong>Note that this method may be ambiguous</strong> since the same EPSG code can be used for different
* kinds of objects. This method throws an exception if it detects an ambiguity on a <em>best-effort</em> basis.
* It is recommended to invoke the most specific {@code createFoo(String)} method when the desired type is known,
* both for performance reason and for avoiding ambiguity.</p>
*
* @param code value allocated by EPSG.
* @return the object for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see #createCoordinateReferenceSystem(String)
* @see #createDatum(String)
* @see #createCoordinateSystem(String)
*/
@Override
public synchronized IdentifiedObject createObject(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
final boolean isPrimaryKey = isPrimaryKey(code);
final StringBuilder query = new StringBuilder("SELECT ");
final int queryStart = query.length();
int found = -1;
try {
final int pk = isPrimaryKey ? toPrimaryKeys(null, null, null, code)[0] : 0;
for (int i = 0; i < TableInfo.EPSG.length; i++) {
final TableInfo table = TableInfo.EPSG[i];
final String column = isPrimaryKey ? table.codeColumn : table.nameColumn;
if (column == null) {
continue;
}
query.setLength(queryStart);
query.append(table.codeColumn);
if (!isPrimaryKey) {
query.append(", ").append(column); // Only for filterFalsePositive(…).
}
query.append(" FROM ").append(table.table)
.append(" WHERE ").append(column).append(isPrimaryKey ? " = ?" : " LIKE ?");
try (PreparedStatement stmt = connection.prepareStatement(translator.apply(query.toString()))) {
/*
* Check if at least one record is found for the code or the name.
* Ensure that there is not two values for the same code or name.
*/
if (isPrimaryKey) {
stmt.setInt(1, pk);
} else {
stmt.setString(1, toLikePattern(code));
}
Integer present = null;
try (ResultSet result = stmt.executeQuery()) {
while (result.next()) {
if (isPrimaryKey || SQLUtilities.filterFalsePositive(code, result.getString(2))) {
present = ensureSingleton(getOptionalInteger(result, 1), present, code);
}
}
}
if (present != null) {
if (found >= 0) {
throw new FactoryDataException(error().getString(Errors.Keys.DuplicatedIdentifier_1, code));
}
found = i;
}
}
}
} 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 (found >= 0) {
switch (found) {
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 createOperationMethod (code);
case 8: return createParameterDescriptor (code);
case 9: break; // Can not cast Unit to IdentifiedObject
default: throw new AssertionError(found); // Should not happen
}
}
throw noSuchAuthorityCode(IdentifiedObject.class, code);
}
/**
* Creates an arbitrary coordinate reference system from a code.
* The returned object will typically be an instance of {@link GeographicCRS}, {@link ProjectedCRS},
* {@link VerticalCRS} or {@link CompoundCRS}.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for coordinate reference systems are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Type</th> <th>Description</th></tr>
* <tr><td>4326</td> <td>Geographic</td> <td>World Geodetic System 1984</td></tr>
* <tr><td>4979</td> <td>Geographic 3D</td> <td>World Geodetic System 1984</td></tr>
* <tr><td>4978</td> <td>Geocentric</td> <td>World Geodetic System 1984</td></tr>
* <tr><td>3395</td> <td>Projected</td> <td>WGS 84 / World Mercator</td></tr>
* <tr><td>5714</td> <td>Vertical</td> <td>Mean Sea Level height</td></tr>
* <tr><td>6349</td> <td>Compound</td> <td>NAD83(2011) + NAVD88 height</td></tr>
* <tr><td>5800</td> <td>Engineering</td> <td>Astra Minas Grid</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the coordinate reference system for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized CoordinateReferenceSystem createCoordinateReferenceSystem(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
CoordinateReferenceSystem returnValue = null;
try (ResultSet result = executeQuery("Coordinate Reference System", "COORD_REF_SYS_CODE", "COORD_REF_SYS_NAME",
"SELECT COORD_REF_SYS_CODE," + // [ 1]
" COORD_REF_SYS_NAME," + // [ 2]
" AREA_OF_USE_CODE," + // [ 3]
" CRS_SCOPE," + // [ 4]
" REMARKS," + // [ 5]
" DEPRECATED," + // [ 6]
" COORD_REF_SYS_KIND," + // [ 7]
" COORD_SYS_CODE," + // [ 8] Null for CompoundCRS
" DATUM_CODE," + // [ 9] Null for ProjectedCRS
" SOURCE_GEOGCRS_CODE," + // [10] For ProjectedCRS
" PROJECTION_CONV_CODE," + // [11] For ProjectedCRS
" CMPD_HORIZCRS_CODE," + // [12] For CompoundCRS only
" CMPD_VERTCRS_CODE" + // [13] For CompoundCRS only
" FROM [Coordinate Reference System]" +
" WHERE COORD_REF_SYS_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final String area = getOptionalString (result, 3);
final String scope = getOptionalString (result, 4);
final String remarks = getOptionalString (result, 5);
final boolean deprecated = getOptionalBoolean(result, 6);
final String type = getString (code, result, 7);
/*
* Note: Do not invoke 'createProperties' now, even if we have all required information,
* because the 'properties' map is going to overwritten by calls to 'createDatum', etc.
*
* The following switch statement should have a case for all "epsg_crs_kind" values enumerated
* in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
*/
final CRSFactory crsFactory = owner.crsFactory;
final CoordinateReferenceSystem crs;
switch (type.toLowerCase(Locale.US)) {
/* ----------------------------------------------------------------------
* GEOGRAPHIC CRS
*
* NOTE: 'createProperties' MUST be invoked after any call to an other
* 'createFoo' method. Consequently, do not factor out.
* ---------------------------------------------------------------------- */
case "geographic 2d":
case "geographic 3d": {
Integer csCode = getInteger(code, result, 8);
if (replaceDeprecatedCS) {
csCode = DEPRECATED_CS.getOrDefault(csCode, csCode);
}
final EllipsoidalCS cs = owner.createEllipsoidalCS(csCode.toString());
final String datumCode = getOptionalString(result, 9);
final GeodeticDatum datum;
if (datumCode != null) {
datum = owner.createGeodeticDatum(datumCode);
} else {
final String geoCode = getString(code, result, 10, 9);
result.close(); // Must be closed before call to createGeographicCRS(String)
ensureNoCycle(GeographicCRS.class, epsg);
try {
datum = owner.createGeographicCRS(geoCode).getDatum();
} finally {
endOfRecursivity(GeographicCRS.class, epsg);
}
}
crs = crsFactory.createGeographicCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), datum, cs);
break;
}
/* ----------------------------------------------------------------------
* PROJECTED CRS
*
* NOTE: This method invokes itself indirectly, through createGeographicCRS.
* Consequently we can not use 'result' anymore after this block.
* ---------------------------------------------------------------------- */
case "projected": {
final String csCode = getString(code, result, 8);
final String geoCode = getString(code, result, 10);
final String opCode = getString(code, result, 11);
result.close(); // Must be closed before call to createFoo(String)
ensureNoCycle(ProjectedCRS.class, epsg);
try {
final CartesianCS cs = owner.createCartesianCS(csCode);
final Conversion op;
try {
op = (Conversion) owner.createCoordinateOperation(opCode);
} catch (ClassCastException e) {
// Should never happen in a well-formed EPSG database.
// If happen anyway, the ClassCastException cause will give more hints than just the message.
throw (NoSuchAuthorityCodeException) noSuchAuthorityCode(Projection.class, opCode).initCause(e);
}
final CoordinateReferenceSystem baseCRS;
final boolean resumeParamChecks;
if (!deprecated) {
baseCRS = owner.createCoordinateReferenceSystem(geoCode);
resumeParamChecks = false;
} else {
/*
* If the ProjectedCRS is deprecated, one reason among others may be that it uses one of
* the deprecated coordinate systems. Those deprecated CS used non-linear units like DMS.
* Apache SIS can not instantiate a ProjectedCRS when the baseCRS uses such units, so we
* set a flag asking to replace the deprecated CS by a supported one. Since that baseCRS
* would not be exactly as defined by EPSG, we must not cache it because we do not want
* 'owner.createGeographicCRS(geoCode)' to return that modified CRS. Since the same CRS
* may be recreated every time a deprecated ProjectedCRS is created, we temporarily
* shutdown the loggings in order to avoid the same warning to be logged many time.
*/
final boolean old = quiet;
try {
quiet = true;
replaceDeprecatedCS = true;
baseCRS = createCoordinateReferenceSystem(geoCode); // Do not cache that CRS.
} finally {
replaceDeprecatedCS = false;
quiet = old;
}
/*
* The crsFactory method calls will indirectly create a parameterized MathTransform.
* Their constructor will try to verify the parameter validity. But some deprecated
* CRS had invalid parameter values (they were deprecated precisely for that reason).
* If and only if we are creating a deprecated CRS, temporarily suspend the parameter
* checks.
*/
resumeParamChecks = !Semaphores.queryAndSet(Semaphores.SUSPEND_PARAMETER_CHECK);
// Try block must be immediately after above line (do not insert any code between).
}
try {
/*
* For a ProjectedCRS, the baseCRS is always geographic. So in theory we would not
* need the 'instanceof' check. However the EPSG dataset version 8.9 also uses the
* "projected" type for CRS that are actually derived CRS. See EPSG:5820 and 5821.
*/
final Map<String, Object> properties = createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated);
if (baseCRS instanceof GeographicCRS) {
crs = crsFactory.createProjectedCRS(properties, (GeographicCRS) baseCRS, op, cs);
} else {
crs = crsFactory.createDerivedCRS(properties, baseCRS, op, cs);
}
} finally {
if (resumeParamChecks) {
Semaphores.clear(Semaphores.SUSPEND_PARAMETER_CHECK);
}
}
} finally {
endOfRecursivity(ProjectedCRS.class, epsg);
}
break;
}
/* ----------------------------------------------------------------------
* VERTICAL CRS
* ---------------------------------------------------------------------- */
case "vertical": {
final VerticalCS cs = owner.createVerticalCS (getString(code, result, 8));
final VerticalDatum datum = owner.createVerticalDatum(getString(code, result, 9));
crs = crsFactory.createVerticalCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), datum, cs);
break;
}
/* ----------------------------------------------------------------------
* TEMPORAL CRS
*
* NOTE : The original EPSG database does not define any temporal CRS.
* This block is a SIS-specific extension.
* ---------------------------------------------------------------------- */
case "time":
case "temporal": {
final TimeCS cs = owner.createTimeCS (getString(code, result, 8));
final TemporalDatum datum = owner.createTemporalDatum(getString(code, result, 9));
crs = crsFactory.createTemporalCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), datum, cs);
break;
}
/* ----------------------------------------------------------------------
* COMPOUND CRS
*
* NOTE: This method invokes itself recursively.
* Consequently, we can not use 'result' anymore.
* ---------------------------------------------------------------------- */
case "compound": {
final String code1 = getString(code, result, 12);
final String code2 = getString(code, result, 13);
result.close();
final CoordinateReferenceSystem crs1, crs2;
ensureNoCycle(CompoundCRS.class, epsg);
try {
crs1 = owner.createCoordinateReferenceSystem(code1);
crs2 = owner.createCoordinateReferenceSystem(code2);
} finally {
endOfRecursivity(CompoundCRS.class, epsg);
}
// Note: Do not invoke 'createProperties' sooner.
crs = crsFactory.createCompoundCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), crs1, crs2);
break;
}
/* ----------------------------------------------------------------------
* GEOCENTRIC CRS
* ---------------------------------------------------------------------- */
case "geocentric": {
final CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8));
final GeodeticDatum datum = owner.createGeodeticDatum (getString(code, result, 9));
final Map<String,Object> properties = createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated);
if (cs instanceof CartesianCS) {
crs = crsFactory.createGeocentricCRS(properties, datum, (CartesianCS) cs);
} else if (cs instanceof SphericalCS) {
crs = crsFactory.createGeocentricCRS(properties, datum, (SphericalCS) cs);
} else {
throw new FactoryDataException(error().getString(
Errors.Keys.IllegalCoordinateSystem_1, cs.getName()));
}
break;
}
/* ----------------------------------------------------------------------
* ENGINEERING CRS
* ---------------------------------------------------------------------- */
case "engineering": {
final CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8));
final EngineeringDatum datum = owner.createEngineeringDatum(getString(code, result, 9));
crs = crsFactory.createEngineeringCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), datum, cs);
break;
}
/* ----------------------------------------------------------------------
* PARAMETRIC CRS
* ---------------------------------------------------------------------- */
case "parametric": {
final DefaultParametricCS cs = owner.createParametricCS (getString(code, result, 8));
final DefaultParametricDatum datum = owner.createParametricDatum(getString(code, result, 9));
crs = ServicesForMetadata.createParametricCRS(createProperties("Coordinate Reference System",
name, epsg, area, scope, remarks, deprecated), datum, cs, crsFactory);
break;
}
/* ----------------------------------------------------------------------
* UNKNOWN CRS
* ---------------------------------------------------------------------- */
default: {
throw new FactoryDataException(error().getString(Errors.Keys.UnknownType_1, type));
}
}
returnValue = ensureSingleton(crs, returnValue, code);
if (result.isClosed()) {
return returnValue;
}
}
} catch (SQLException exception) {
throw databaseFailure(CoordinateReferenceSystem.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(CoordinateReferenceSystem.class, code);
}
return returnValue;
}
/**
* Creates an arbitrary datum from a code. The returned object will typically be an
* instance of {@link GeodeticDatum}, {@link VerticalDatum} or {@link TemporalDatum}.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for datums are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Type</th> <th>Description</th></tr>
* <tr><td>6326</td> <td>Geodetic</td> <td>World Geodetic System 1984</td></tr>
* <tr><td>6322</td> <td>Geodetic</td> <td>World Geodetic System 1972</td></tr>
* <tr><td>1027</td> <td>Vertical</td> <td>EGM2008 geoid</td></tr>
* <tr><td>5100</td> <td>Vertical</td> <td>Mean Sea Level</td></tr>
* <tr><td>9315</td> <td>Engineering</td> <td>Seismic bin grid datum</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the datum for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized Datum createDatum(final String code) throws NoSuchAuthorityCodeException, FactoryException {
ArgumentChecks.ensureNonNull("code", code);
Datum returnValue = null;
try (ResultSet result = executeQuery("Datum", "DATUM_CODE", "DATUM_NAME",
"SELECT DATUM_CODE," +
" DATUM_NAME," +
" DATUM_TYPE," +
" ORIGIN_DESCRIPTION," +
" REALIZATION_EPOCH," +
" AREA_OF_USE_CODE," +
" DATUM_SCOPE," +
" REMARKS," +
" DEPRECATED," +
" ELLIPSOID_CODE," + // Only for geodetic type
" PRIME_MERIDIAN_CODE" + // Only for geodetic type
" FROM [Datum]" +
" WHERE DATUM_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final String type = getString (code, result, 3);
final String anchor = getOptionalString (result, 4);
final String epoch = getOptionalString (result, 5);
final String area = getOptionalString (result, 6);
final String scope = getOptionalString (result, 7);
final String remarks = getOptionalString (result, 8);
final boolean deprecated = getOptionalBoolean(result, 9);
Map<String,Object> properties = createProperties("Datum",
name, epsg, area, scope, remarks, deprecated);
if (anchor != null) {
properties.put(Datum.ANCHOR_POINT_KEY, anchor);
}
if (epoch != null) try {
/*
* Parse the date manually because it is declared as a VARCHAR instead than DATE in original
* SQL scripts. Apache SIS installer replaces VARCHAR by DATE, but we have no guarantee that
* we are reading an EPSG database created by our installer. Furthermore an older version of
* EPSG installer was using SMALLINT instead than DATE, because scripts before EPSG 9.0 were
* reporting only the epoch year.
*/
final CharSequence[] fields = CharSequences.split(epoch, '-');
int year = 0, month = 0, day = 1;
for (int i = Math.min(fields.length, 3); --i >= 0;) {
final int f = Integer.parseInt(fields[i].toString());
switch (i) {
case 0: year = f; break;
case 1: month = f-1; break;
case 2: day = f; break;
}
}
if (year != 0) {
final Calendar calendar = getCalendar();
calendar.set(year, month, day);
properties.put(Datum.REALIZATION_EPOCH_KEY, calendar.getTime());
}
} catch (NumberFormatException exception) {
unexpectedException("createDatum", exception); // Not a fatal error.
}
/*
* The following switch statement should have a case for all "epsg_datum_kind" values enumerated
* in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
*/
final DatumFactory datumFactory = owner.datumFactory;
final Datum datum;
switch (type.toLowerCase(Locale.US)) {
/*
* The "geodetic" case invokes createProperties(…) indirectly through calls to
* createEllipsoid(String) and createPrimeMeridian(String), so we must protect
* the properties map from changes.
*/
case "geodetic": {
properties = new HashMap<>(properties); // Protect from changes
final Ellipsoid ellipsoid = owner.createEllipsoid (getString(code, result, 10));
final PrimeMeridian meridian = owner.createPrimeMeridian(getString(code, result, 11));
final BursaWolfParameters[] param = createBursaWolfParameters(meridian, epsg);
if (param != null) {
properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, param);
}
datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
break;
}
/*
* Vertical datum type is hard-coded to geoidal. It would be possible to infer other
* types by looking at the coordinate system, but it could result in different datum
* associated to the same EPSG code. Since vertical datum type is no longer part of
* ISO 19111:2007, it is probably not worth to handle such cases.
*/
case "vertical": {
datum = datumFactory.createVerticalDatum(properties, VERTICAL_DATUM_TYPE);
break;
}
/*
* Origin date is stored in ORIGIN_DESCRIPTION field. A column of SQL type
* "date" type would have been better, but we do not modify the EPSG model.
*/
case "temporal": {
final Date originDate;
if (anchor == null || anchor.isEmpty()) {
throw new FactoryDataException(resources().getString(Resources.Keys.DatumOriginShallBeDate));
}
if (dateFormat == null) {
dateFormat = new StandardDateFormat();
dateFormat.setCalendar(getCalendar()); // Use UTC timezone.
}
try {
originDate = dateFormat.parse(anchor);
} catch (ParseException e) {
throw new FactoryDataException(resources().getString(Resources.Keys.DatumOriginShallBeDate), e);
}
datum = datumFactory.createTemporalDatum(properties, originDate);
break;
}
/*
* Straightforward case.
*/
case "engineering": {
datum = datumFactory.createEngineeringDatum(properties);
break;
}
case "parametric": {
datum = ServicesForMetadata.createParametricDatum(properties, datumFactory);
break;
}
default: {
throw new FactoryDataException(error().getString(Errors.Keys.UnknownType_1, type));
}
}
returnValue = ensureSingleton(datum, returnValue, code);
if (result.isClosed()) {
break; // Because of the recursive call done by createBursaWolfParameters(…).
}
}
} catch (SQLException exception) {
throw databaseFailure(Datum.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(Datum.class, code);
}
return returnValue;
}
/**
* Returns Bursa-Wolf parameters for a geodetic datum. If the specified datum has no conversion information,
* then this method returns {@code null}.
*
* <p>This method is for compatibility with <cite>Well Known Text</cite> (WKT) version 1 formatting.
* That legacy format had a {@code TOWGS84} element which needs the information provided by this method.
* Note that {@code TOWGS84} is a deprecated element as of WKT 2 (ISO 19162).</p>
*
* @param meridian the source datum prime meridian, used for discarding any target datum using a different meridian.
* @param code the EPSG code of the source {@link GeodeticDatum}.
* @return an array of Bursa-Wolf parameters, or {@code null}.
*/
private BursaWolfParameters[] createBursaWolfParameters(final PrimeMeridian meridian, final Integer code)
throws SQLException, FactoryException
{
/*
* We do not provide TOWGS84 information for WGS84 itself or for any other datum on our list of target datum,
* in order to avoid infinite recursivity. The 'ensureNonRecursive' call is an extra safety check which should
* never fail, unless TARGET_CRS and TARGET_DATUM values do not agree with database content.
*/
if (code == BursaWolfInfo.TARGET_DATUM) {
return null;
}
final List<BursaWolfInfo> bwInfos = new ArrayList<>();
try (ResultSet result = executeQuery("BursaWolfParametersSet",
"SELECT COORD_OP_CODE," +
" COORD_OP_METHOD_CODE," +
" TARGET_CRS_CODE," +
" AREA_OF_USE_CODE"+
" FROM [Coordinate_Operation]" +
" WHERE DEPRECATED=0" + // Do not put spaces around "=" - SQLTranslator searches for this exact match.
" AND TARGET_CRS_CODE = " + BursaWolfInfo.TARGET_CRS +
" AND COORD_OP_METHOD_CODE >= " + BursaWolfInfo.MIN_METHOD_CODE +
" AND COORD_OP_METHOD_CODE <= " + BursaWolfInfo.MAX_METHOD_CODE +
" AND SOURCE_CRS_CODE IN " +
"(SELECT COORD_REF_SYS_CODE FROM [Coordinate Reference System] WHERE DATUM_CODE = ?)" +
" ORDER BY TARGET_CRS_CODE, COORD_OP_ACCURACY, COORD_OP_CODE DESC", code))
{
while (result.next()) {
final BursaWolfInfo info = new BursaWolfInfo(
getInteger(code, result, 1), // Operation
getInteger(code, result, 2), // Method
getInteger(code, result, 3), // Target datum
getInteger(code, result, 4)); // Domain of validity
if (info.target != code) { // Paranoiac check.
bwInfos.add(info);
}
}
}
int size = bwInfos.size();
if (size == 0) {
return null;
}
/*
* Sort 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.
*/
if (size > 1) {
final BursaWolfInfo[] codes = bwInfos.toArray(new BursaWolfInfo[size]);
sort("Coordinate_Operation", codes);
bwInfos.clear();
BursaWolfInfo.filter(owner, codes, bwInfos);
size = bwInfos.size();
}
/*
* Now, iterate over the results and fetch the parameter values for each BursaWolfParameters object.
*/
final BursaWolfParameters[] parameters = new BursaWolfParameters[size];
final Locale locale = getLocale();
int count = 0;
for (int i=0; i<size; i++) {
final BursaWolfInfo info = bwInfos.get(i);
final GeodeticDatum datum;
ensureNoCycle(BursaWolfParameters.class, code); // See comment at the begining of this method.
try {
datum = owner.createGeodeticDatum(String.valueOf(info.target));
} finally {
endOfRecursivity(BursaWolfParameters.class, code);
}
/*
* Accept only Bursa-Wolf parameters between datum that use the same prime meridian.
* This is for avoiding ambiguity about whether longitude rotation should be applied
* before or after the datum change. This check is useless for EPSG dataset 8.9 since
* all datum seen by this method use Greenwich. But we nevertheless perform this check
* as a safety for future evolution or customized EPSG dataset.
*/
if (!equalsIgnoreMetadata(meridian, datum.getPrimeMeridian())) {
continue;
}
final BursaWolfParameters bwp = new BursaWolfParameters(datum, info.getDomainOfValidity(owner));
try (ResultSet result = executeQuery("BursaWolfParameters",
"SELECT PARAMETER_CODE," +
" PARAMETER_VALUE," +
" UOM_CODE" +
" FROM [Coordinate_Operation Parameter Value]" +
" WHERE COORD_OP_CODE = ?" +
" AND COORD_OP_METHOD_CODE = ?", info.operation, info.method))
{
while (result.next()) {
BursaWolfInfo.setBursaWolfParameter(bwp,
getInteger(info.operation, result, 1),
getDouble (info.operation, result, 2),
owner.createUnit(getString(info.operation, result, 3)), locale);
}
}
if (info.isFrameRotation()) {
// Coordinate frame rotation (9607): same as 9606,
// except for the sign of rotation parameters.
bwp.reverseRotation();
}
parameters[count++] = bwp;
}
return ArraysExt.resize(parameters, count);
}
/**
* Creates a geometric figure that can be used to describe the approximate shape of the earth.
* In mathematical terms, it is a surface formed by the rotation of an ellipse about its minor axis.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for ellipsoids are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>7030</td> <td>WGS 84</td></tr>
* <tr><td>7034</td> <td>Clarke 1880</td></tr>
* <tr><td>7048</td> <td>GRS 1980 Authalic Sphere</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the ellipsoid for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see #createGeodeticDatum(String)
* @see #createEllipsoidalCS(String)
* @see org.apache.sis.referencing.datum.DefaultEllipsoid
*/
@Override
public synchronized Ellipsoid createEllipsoid(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
Ellipsoid returnValue = null;
try (ResultSet result = executeQuery("Ellipsoid", "ELLIPSOID_CODE", "ELLIPSOID_NAME",
"SELECT ELLIPSOID_CODE," +
" ELLIPSOID_NAME," +
" SEMI_MAJOR_AXIS," +
" INV_FLATTENING," +
" SEMI_MINOR_AXIS," +
" UOM_CODE," +
" REMARKS," +
" DEPRECATED" +
" FROM [Ellipsoid]" +
" WHERE ELLIPSOID_CODE = ?", code))
{
while (result.next()) {
/*
* One of 'semiMinorAxis' and 'inverseFlattening' values can be NULL in the database.
* Consequently, we don't use 'getString(ResultSet, int)' for those parameters because
* we do not want to thrown an exception if a NULL value is found.
*/
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final double semiMajorAxis = getDouble (code, result, 3);
final double inverseFlattening = getOptionalDouble (result, 4);
final double semiMinorAxis = getOptionalDouble (result, 5);
final String unitCode = getString (code, result, 6);
final String remarks = getOptionalString (result, 7);
final boolean deprecated = getOptionalBoolean(result, 8);
final Unit<Length> unit = owner.createUnit(unitCode).asType(Length.class);
final Map<String,Object> properties = createProperties("Ellipsoid", name, epsg, remarks, deprecated);
final Ellipsoid ellipsoid;
if (Double.isNaN(inverseFlattening)) {
if (Double.isNaN(semiMinorAxis)) {
// Both are null, which is not allowed.
final String column = result.getMetaData().getColumnName(3);
throw new FactoryDataException(error().getString(Errors.Keys.NullValueInTable_3, code, column));
} else {
// We only have semiMinorAxis defined. It is OK
ellipsoid = owner.datumFactory.createEllipsoid(properties, semiMajorAxis, semiMinorAxis, unit);
}
} else {
if (!Double.isNaN(semiMinorAxis)) {
// Both 'inverseFlattening' and 'semiMinorAxis' are defined.
// Log a warning and create the ellipsoid using the inverse flattening.
final LogRecord record = resources().getLogRecord(Level.WARNING,
Resources.Keys.AmbiguousEllipsoid_1, Constants.EPSG + Constants.DEFAULT_SEPARATOR + code);
record.setLoggerName(Loggers.CRS_FACTORY);
Logging.log(EPSGDataAccess.class, "createEllipsoid", record);
}
ellipsoid = owner.datumFactory.createFlattenedSphere(properties, semiMajorAxis, inverseFlattening, unit);
}
returnValue = ensureSingleton(ellipsoid, returnValue, code);
}
} catch (SQLException exception) {
throw databaseFailure(Ellipsoid.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(Ellipsoid.class, code);
}
return returnValue;
}
/**
* Creates a prime meridian defining the origin from which longitude values are determined.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for prime meridians are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>8901</td> <td>Greenwich</td></tr>
* <tr><td>8903</td> <td>Paris</td></tr>
* <tr><td>8904</td> <td>Bogota</td></tr>
* <tr><td>8905</td> <td>Madrid</td></tr>
* <tr><td>8906</td> <td>Rome</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the prime meridian for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see #createGeodeticDatum(String)
* @see org.apache.sis.referencing.datum.DefaultPrimeMeridian
*/
@Override
public synchronized PrimeMeridian createPrimeMeridian(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
PrimeMeridian returnValue = null;
try (ResultSet result = executeQuery("Prime Meridian", "PRIME_MERIDIAN_CODE", "PRIME_MERIDIAN_NAME",
"SELECT PRIME_MERIDIAN_CODE," +
" PRIME_MERIDIAN_NAME," +
" GREENWICH_LONGITUDE," +
" UOM_CODE," +
" REMARKS," +
" DEPRECATED" +
" FROM [Prime Meridian]" +
" WHERE PRIME_MERIDIAN_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final double longitude = getDouble (code, result, 3);
final String unitCode = getString (code, result, 4);
final String remarks = getOptionalString (result, 5);
final boolean deprecated = getOptionalBoolean(result, 6);
final Unit<Angle> unit = owner.createUnit(unitCode).asType(Angle.class);
final PrimeMeridian primeMeridian = owner.datumFactory.createPrimeMeridian(
createProperties("Prime Meridian", name, epsg, remarks, deprecated), longitude, unit);
returnValue = ensureSingleton(primeMeridian, returnValue, code);
}
} catch (SQLException exception) {
throw databaseFailure(PrimeMeridian.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(PrimeMeridian.class, code);
}
return returnValue;
}
/**
* Creates information about spatial, vertical, and temporal extent (usually a domain of validity) from a code.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for extents are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>1262</td> <td>World</td></tr>
* <tr><td>3391</td> <td>World - between 80°S and 84°N</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the extent for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see #createCoordinateReferenceSystem(String)
* @see #createDatum(String)
* @see org.apache.sis.metadata.iso.extent.DefaultExtent
*/
@Override
public synchronized Extent createExtent(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
Extent returnValue = null;
try (ResultSet result = executeQuery("Area", "AREA_CODE", "AREA_NAME",
"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 = ?", code))
{
while (result.next()) {
final String description = getOptionalString(result, 1);
double ymin = getOptionalDouble(result, 2);
double ymax = getOptionalDouble(result, 3);
double xmin = getOptionalDouble(result, 4);
double xmax = getOptionalDouble(result, 5);
DefaultGeographicBoundingBox bbox = null;
if (!Double.isNaN(ymin) || !Double.isNaN(ymax) || !Double.isNaN(xmin) || !Double.isNaN(xmax)) {
/*
* Fix an error found in EPSG:3790 New Zealand - South Island - Mount Pleasant mc
* for older database (this error is fixed in EPSG database 8.2).
*
* Do NOT apply anything similar for the x axis, because xmin > xmax is not error:
* it describes a bounding box spanning the anti-meridian (±180° of longitude).
*/
if (ymin > ymax) {
final double t = ymin;
ymin = ymax;
ymax = t;
}
bbox = new DefaultGeographicBoundingBox(xmin, xmax, ymin, ymax);
}
if (description != null || bbox != null) {
DefaultExtent extent = new DefaultExtent(description, bbox, null, null);
extent.transitionTo(DefaultExtent.State.FINAL);
returnValue = ensureSingleton(extent, returnValue, code);
}
}
} catch (SQLException exception) {
throw databaseFailure(Extent.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(Extent.class, code);
}
return returnValue;
}
/**
* Creates an arbitrary coordinate system from a code. The returned object will typically be an
* instance of {@link EllipsoidalCS}, {@link CartesianCS} or {@link VerticalCS}.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for coordinate systems are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Type</th> <th>Axes</th> <th>Orientations</th> <th>Unit</th></tr>
* <tr><td>4406</td> <td>Cartesian 2D CS</td> <td>easting, northing (E,N)</td> <td>east, north</td> <td>kilometre</td></tr>
* <tr><td>4496</td> <td>Cartesian 2D CS</td> <td>easting, northing (E,N)</td> <td>east, north</td> <td>metre</td></tr>
* <tr><td>4500</td> <td>Cartesian 2D CS</td> <td>northing, easting (N,E)</td> <td>north, east</td> <td>metre</td></tr>
* <tr><td>4491</td> <td>Cartesian 2D CS</td> <td>westing, northing (W,N)</td> <td>west, north</td> <td>metre</td></tr>
* <tr><td>6422</td> <td>Ellipsoidal 2D CS</td> <td>latitude, longitude</td> <td>north, east</td> <td>degree</td></tr>
* <tr><td>6424</td> <td>Ellipsoidal 2D CS</td> <td>longitude, latitude</td> <td>east, north</td> <td>degree</td></tr>
* <tr><td>6429</td> <td>Ellipsoidal 2D CS</td> <td>longitude, latitude</td> <td>east, north</td> <td>radian</td></tr>
* <tr><td>6423</td> <td>Ellipsoidal 3D CS</td> <td>latitude, longitude, ellipsoidal height</td> <td>north, east, up</td> <td>degree, degree, metre</td></tr>
* <tr><td>6404</td> <td>Spherical 3D CS</td> <td>latitude, longitude, radius</td> <td>north, east, up</td> <td>degree, degree, metre</td></tr>
* <tr><td>6498</td> <td>Vertical CS</td> <td>depth (D)</td> <td>down</td> <td>metre</td></tr>
* <tr><td>6499</td> <td>Vertical CS</td> <td>height (H)</td> <td>up</td> <td>metre</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the coordinate system for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized CoordinateSystem createCoordinateSystem(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
CoordinateSystem returnValue = null;
try (ResultSet result = executeQuery("Coordinate System", "COORD_SYS_CODE", "COORD_SYS_NAME",
"SELECT COORD_SYS_CODE," +
" COORD_SYS_NAME," +
" COORD_SYS_TYPE," +
" DIMENSION," +
" REMARKS," +
" DEPRECATED" +
" FROM [Coordinate System]" +
" WHERE COORD_SYS_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final String type = getString (code, result, 3);
final int dimension = getInteger (code, result, 4);
final String remarks = getOptionalString (result, 5);
final boolean deprecated = getOptionalBoolean(result, 6);
final CoordinateSystemAxis[] axes = createCoordinateSystemAxes(epsg, dimension);
final Map<String,Object> properties = createProperties("Coordinate System", name, epsg, remarks, deprecated); // Must be after axes.
/*
* The following switch statement should have a case for all "epsg_cs_kind" values enumerated
* in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
*/
final CSFactory csFactory = owner.csFactory;
CoordinateSystem cs = null;
switch (type.toLowerCase(Locale.US)) {
case WKTKeywords.ellipsoidal: {
switch (dimension) {
case 2: cs = csFactory.createEllipsoidalCS(properties, axes[0], axes[1]); break;
case 3: cs = csFactory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]); break;
}
break;
}
case "cartesian": { // Need lower-case "c"
switch (dimension) {
case 2: cs = csFactory.createCartesianCS(properties, axes[0], axes[1]); break;
case 3: cs = csFactory.createCartesianCS(properties, axes[0], axes[1], axes[2]); break;
}
break;
}
case WKTKeywords.spherical: {
switch (dimension) {
case 3: cs = csFactory.createSphericalCS(properties, axes[0], axes[1], axes[2]); break;
}
break;
}
case WKTKeywords.vertical:
case "gravity-related": {
switch (dimension) {
case 1: cs = csFactory.createVerticalCS(properties, axes[0]); break;
}
break;
}
case "time":
case WKTKeywords.temporal: {
switch (dimension) {
case 1: cs = csFactory.createTimeCS(properties, axes[0]); break;
}
break;
}
case WKTKeywords.parametric: {
switch (dimension) {
case 1: cs = ServicesForMetadata.createParametricCS(properties, axes[0], csFactory); break;
}
break;
}
case WKTKeywords.linear: {
switch (dimension) {
case 1: cs = csFactory.createLinearCS(properties, axes[0]); break;
}
break;
}
case WKTKeywords.polar: {
switch (dimension) {
case 2: cs = csFactory.createPolarCS(properties, axes[0], axes[1]); break;
}
break;
}
case WKTKeywords.cylindrical: {
switch (dimension) {
case 3: cs = csFactory.createCylindricalCS(properties, axes[0], axes[1], axes[2]); break;
}
break;
}
case WKTKeywords.affine: {
switch (dimension) {
case 2: cs = csFactory.createAffineCS(properties, axes[0], axes[1]); break;
case 3: cs = csFactory.createAffineCS(properties, axes[0], axes[1], axes[2]); break;
}
break;
}
default: {
throw new FactoryDataException(error().getString(Errors.Keys.UnknownType_1, type));
}
}
if (cs == null) {
throw new FactoryDataException(resources().getString(Resources.Keys.UnexpectedDimensionForCS_1, type));
}
returnValue = ensureSingleton(cs, returnValue, code);
}
} catch (SQLException exception) {
throw databaseFailure(CoordinateSystem.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(CoordinateSystem.class, code);
}
return returnValue;
}
/**
* Returns the number of dimension for the specified Coordinate System, or {@code null} if not found.
*
* @param cs the EPSG code for the coordinate system.
* @return the number of dimensions, or {@code null} if not found.
*
* @see #getDimensionsForMethod(Integer)
*/
private Integer getDimensionForCS(final Integer cs) throws SQLException {
Integer dimension = csDimensions.get(cs);
if (dimension == null) {
try (ResultSet result = executeQuery("Dimension",
" SELECT COUNT(COORD_AXIS_CODE)" +
" FROM [Coordinate Axis]" +
" WHERE COORD_SYS_CODE = ?", cs))
{
dimension = result.next() ? result.getInt(1) : 0;
csDimensions.put(cs, dimension);
}
}
return (dimension != 0) ? dimension : null;
}
/**
* 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 does not accept that name.</p>
*
* @param cs the EPSG code for the coordinate system.
* @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 occurred during database access.
* @throws FactoryException if the code has not been found.
*/
private CoordinateSystemAxis[] createCoordinateSystemAxes(final Integer cs, final int dimension)
throws SQLException, FactoryException
{
int i = 0;
final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[dimension];
try (ResultSet result = executeQuery("AxisOrder",
"SELECT COORD_AXIS_CODE" +
" FROM [Coordinate Axis]" +
" WHERE COORD_SYS_CODE = ?" +
" ORDER BY [ORDER]", cs))
{
while (result.next()) {
final String axis = getString(cs, result, 1);
if (i < axes.length) {
/*
* If 'i' is out of bounds, an exception will be thrown after the loop.
* We do not want to thrown an ArrayIndexOutOfBoundsException here.
*/
axes[i] = owner.createCoordinateSystemAxis(axis);
}
++i;
}
}
if (i != axes.length) {
throw new FactoryDataException(error().getString(Errors.Keys.MismatchedDimension_2, axes.length, i));
}
return axes;
}
/**
* Creates a coordinate system axis with name, direction, unit and range of values.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for axes are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th> <th>Unit</th></tr>
* <tr><td>106</td> <td>Latitude (φ)</td> <td>degree</td></tr>
* <tr><td>107</td> <td>Longitude (λ)</td> <td>degree</td></tr>
* <tr><td>1</td> <td>Easting (E)</td> <td>metre</td></tr>
* <tr><td>2</td> <td>Northing (N)</td> <td>metre</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the axis for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see #createCoordinateSystem(String)
* @see org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis
*/
@Override
public synchronized CoordinateSystemAxis createCoordinateSystemAxis(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
CoordinateSystemAxis returnValue = null;
try (ResultSet result = executeQuery("Coordinate Axis", "COORD_AXIS_CODE", null,
"SELECT COORD_AXIS_CODE," +
" COORD_AXIS_NAME_CODE," +
" COORD_AXIS_ORIENTATION," +
" COORD_AXIS_ABBREVIATION," +
" UOM_CODE" +
" FROM [Coordinate Axis]" +
" WHERE COORD_AXIS_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger(code, result, 1);
final Integer nameCode = getInteger(code, result, 2);
final String orientation = getString (code, result, 3);
final String abbreviation = getString (code, result, 4);
final String unit = getString (code, result, 5);
final AxisDirection direction;
try {
direction = CoordinateSystems.parseAxisDirection(orientation);
} catch (IllegalArgumentException exception) {
throw new FactoryDataException(exception.getLocalizedMessage(), exception);
}
final AxisName an = getAxisName(nameCode);
final CoordinateSystemAxis axis = owner.csFactory.createCoordinateSystemAxis(
createProperties("Coordinate Axis", an.name, epsg, an.description, false),
abbreviation, direction, owner.createUnit(unit));
returnValue = ensureSingleton(axis, returnValue, code);
}
} catch (SQLException exception) {
throw databaseFailure(CoordinateSystemAxis.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(CoordinateSystemAxis.class, code);
}
return returnValue;
}
/**
* Returns the name and description for the specified {@link CoordinateSystemAxis} code.
* Many axes share the same name and description, so it is worth to cache them.
*/
private AxisName getAxisName(final Integer code) throws FactoryException, SQLException {
assert Thread.holdsLock(this);
AxisName returnValue = axisNames.get(code);
if (returnValue == null) {
try (ResultSet result = executeQuery("Coordinate Axis Name",
"SELECT COORD_AXIS_NAME, DESCRIPTION, REMARKS" +
" FROM [Coordinate Axis Name]" +
" WHERE COORD_AXIS_NAME_CODE = ?", code))
{
while (result.next()) {
final String name = getString(code, result, 1);
String description = getOptionalString(result, 2);
String remarks = getOptionalString(result, 3);
if (description == null) {
description = remarks;
} else if (remarks != null) {
description += System.lineSeparator() + remarks;
}
final AxisName axis = new AxisName(name, description);
returnValue = ensureSingleton(axis, returnValue, code);
}
}
if (returnValue == null) {
throw noSuchAuthorityCode(AxisName.class, String.valueOf(code));
}
axisNames.put(code, returnValue);
}
return returnValue;
}
/**
* Creates an unit of measurement from a code.
* Current implementation first checks if {@link Units#valueOfEPSG(int)} can provide a hard-coded unit
* for the given code before to try to parse the information found in the database. This is done that
* way for better support of non-straightforward units like <cite>sexagesimal degrees</cite>
* (EPSG:9110 and 9111).
*
* <div class="note"><b>Example:</b>
* some EPSG codes for units are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>9002</td> <td>decimal degree</td></tr>
* <tr><td>9001</td> <td>metre</td></tr>
* <tr><td>9030</td> <td>kilometre</td></tr>
* <tr><td>1040</td> <td>second</td></tr>
* <tr><td>1029</td> <td>year</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the unit of measurement for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized Unit<?> createUnit(final String code) throws NoSuchAuthorityCodeException, FactoryException {
ArgumentChecks.ensureNonNull("code", code);
Unit<?> returnValue = null;
try (ResultSet result = executeQuery("Unit of Measure", "UOM_CODE", "UNIT_OF_MEAS_NAME",
"SELECT UOM_CODE," +
" FACTOR_B," +
" FACTOR_C," +
" TARGET_UOM_CODE," +
" UNIT_OF_MEAS_NAME" +
" FROM [Unit of Measure]" +
" WHERE UOM_CODE = ?", code))
{
while (result.next()) {
final int source = getInteger(code, result, 1);
final double b = getOptionalDouble(result, 2);
final double c = getOptionalDouble(result, 3);
final int target = getInteger(code, result, 4);
if (source == target) {
/*
* The unit is a base unit. Verify its consistency:
* conversion from 'source' to itself shall be the identity function.
*/
final boolean pb = (b != 1);
if (pb || c != 1) {
throw new FactoryDataException(error().getString(Errors.Keys.InconsistentAttribute_2,
pb ? "FACTOR_B" : "FACTOR_C", pb ? b : c));
}
}
Unit<?> unit = Units.valueOfEPSG(source); // Check in our list of hard-coded unit codes.
if (unit == null) {
final Unit<?> base = Units.valueOfEPSG(target);
if (base != null && !Double.isNaN(b) && !Double.isNaN(c)) { // May be NaN if the conversion is non-linear.
unit = Units.multiply(base, b, c);
} else try {
unit = Units.valueOf(getString(code, result, 5)); // Try parsing the unit symbol as a fallback.
} catch (ParserException e) {
throw new FactoryDataException(error().getString(Errors.Keys.UnknownUnit_1, code), e);
}
}
returnValue = ensureSingleton(unit, returnValue, code);
}
} catch (SQLException exception) {
throw databaseFailure(Unit.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(Unit.class, code);
}
return returnValue;
}
/**
* Creates a definition of a single parameter used by an operation method.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for parameters are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>8801</td> <td>Latitude of natural origin</td></tr>
* <tr><td>8802</td> <td>Longitude of natural origin</td></tr>
* <tr><td>8805</td> <td>Scale factor at natural origin</td></tr>
* <tr><td>8806</td> <td>False easting</td></tr>
* <tr><td>8807</td> <td>False northing</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the parameter descriptor for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*
* @see org.apache.sis.parameter.DefaultParameterDescriptor
*/
@Override
public synchronized ParameterDescriptor<?> createParameterDescriptor(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
ParameterDescriptor<?> returnValue = null;
try (ResultSet result = executeQuery("Coordinate_Operation Parameter", "PARAMETER_CODE", "PARAMETER_NAME",
"SELECT PARAMETER_CODE," +
" PARAMETER_NAME," +
" DESCRIPTION," +
" DEPRECATED" +
" FROM [Coordinate_Operation Parameter]" +
" WHERE PARAMETER_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final String description = getOptionalString (result, 3);
final boolean deprecated = getOptionalBoolean(result, 4);
Class<?> type = Double.class;
/*
* 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 as a string. Otherwise, the type is a floating point number.
*/
try (ResultSet r = executeQuery("ParameterType",
"SELECT PARAM_VALUE_FILE_REF FROM [Coordinate_Operation Parameter Value]" +
" WHERE (PARAMETER_CODE = ?) AND PARAM_VALUE_FILE_REF IS NOT NULL", epsg))
{
while (r.next()) {
String element = getOptionalString(r, 1);
if (element != null && !element.isEmpty()) {
type = String.class;
break;
}
}
}
/*
* Search for units. We typically have many different units but all of the same dimension
* (for example metres, kilometres, feet, etc.). In such case, the units Set will have only
* one element and that element will be the most frequently used unit. But some parameters
* accept units of different dimensions. For example the "Coordinate 1 of evaluation point"
* (EPSG:8617) parameter value may be in metres or in degrees. In such case the units Set
* will have two elements.
*/
final Set<Unit<?>> units = new LinkedHashSet<>();
try (ResultSet r = executeQuery("ParameterUnit",
"SELECT UOM_CODE FROM [Coordinate_Operation Parameter Value]" +
" WHERE (PARAMETER_CODE = ?)" +
" GROUP BY UOM_CODE" +
" ORDER BY COUNT(UOM_CODE) DESC", epsg))
{
next: while (r.next()) {
final String c = getOptionalString(r, 1);
if (c != null) {
final Unit<?> candidate = owner.createUnit(c);
for (final Unit<?> e : units) {
if (candidate.isCompatible(e)) {
continue next;
}
}
units.add(candidate);
}
}
}
/*
* Determines if the inverse operation can be performed by reversing the parameter sign.
* The EPSG dataset uses "Yes" or "No" value, but SIS scripts use boolean type. We have
* to accept both. Note that if we do not recognize the string as a boolean value, then
* we need a SQLException, not a null value. If the value is wrongly null, this method
* will succeed anyway and EPSGDataAccess will finish its work without apparent problem,
* but Apache SIS will fail later when it will try to compute the inverse operation, for
* example in a call to CRS.findOperation(…). The exception thrown at such later time is
* much more difficult to relate to the root cause than if we throw the exception here.
*/
InternationalString isReversible = null;
try (ResultSet r = executeQuery("ParameterSign",
"SELECT DISTINCT PARAM_SIGN_REVERSAL FROM [Coordinate_Operation Parameter Usage]" +
" WHERE (PARAMETER_CODE = ?)", epsg))
{
if (r.next()) {
Boolean b;
if (translator.useBoolean()) {
b = r.getBoolean(1);
if (r.wasNull()) b = null;
} else {
b = SQLUtilities.parseBoolean(r.getString(1)); // May throw SQLException - see above comment.
}
if (b != null) {
isReversible = b ? SignReversalComment.OPPOSITE : SignReversalComment.SAME;
}
}
}
/*
* Now creates the parameter descriptor.
*/
final NumberRange<?> valueDomain;
switch (units.size()) {
case 0: valueDomain = null; break;
default: valueDomain = new EPSGParameterDomain(units); break;
case 1: valueDomain = MeasurementRange.create(Double.NEGATIVE_INFINITY, false,
Double.POSITIVE_INFINITY, false, CollectionsExt.first(units)); break;
}
final Map<String, Object> properties =
createProperties("Coordinate_Operation Parameter", name, epsg, isReversible, deprecated);
properties.put(ImmutableIdentifier.DESCRIPTION_KEY, description);
final ParameterDescriptor<?> descriptor = new DefaultParameterDescriptor<>(properties,
1, 1, type, valueDomain, null, null);
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<?>[] createParameterDescriptors(final Integer method) throws FactoryException, SQLException {
final List<ParameterDescriptor<?>> descriptors = new ArrayList<>();
try (ResultSet result = executeQuery("Coordinate_Operation Parameter Usage",
"SELECT PARAMETER_CODE" +
" FROM [Coordinate_Operation Parameter Usage]" +
" WHERE COORD_OP_METHOD_CODE = ?" +
" ORDER BY SORT_ORDER", method))
{
while (result.next()) {
descriptors.add(owner.createParameterDescriptor(getString(method, result, 1)));
}
}
return descriptors.toArray(new ParameterDescriptor<?>[descriptors.size()]);
}
/**
* Sets the values of all parameters in the given group.
*
* @param method the EPSG code for the operation method.
* @param operation the EPSG code for the operation (conversion or transformation).
* @param parameters the parameter values to fill.
* @throws SQLException if a SQL statement failed.
*/
private void fillParameterValues(final Integer method, final Integer operation, final ParameterValueGroup parameters)
throws FactoryException, SQLException
{
try (ResultSet result = executeQuery("Coordinate_Operation Parameter Value",
"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", method, operation))
{
while (result.next()) {
final String name = getString(operation, result, 1);
final double value = getOptionalDouble(result, 2);
final Unit<?> unit;
String reference;
if (Double.isNaN(value)) {
/*
* If no numeric values were provided in the database, then the values should be
* in some external file. It may be a file in the $SIS_DATA/DatumChanges directory.
*/
reference = getString(operation, result, 3);
unit = null;
} else {
reference = null;
final String unitCode = getOptionalString(result, 4);
unit = (unitCode != null) ? owner.createUnit(unitCode) : null;
}
final ParameterValue<?> param;
try {
param = parameters.parameter(name);
} catch (ParameterNotFoundException exception) {
/*
* Wrap the unchecked ParameterNotFoundException into the checked NoSuchIdentifierException,
* which is a FactoryException subclass. Note that in principle, 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 CoordinateOperationSet to
* know that the failure is probably caused by a MathTransform not yet supported in Apache SIS
* (or only partially supported) rather than some more serious failure in the database side.
* Callers can use this information in order to determine if they should try the next coordinate
* operation or propagate the exception.
*/
throw (NoSuchIdentifierException) new NoSuchIdentifierException(error().getString(
Errors.Keys.CanNotSetParameterValue_1, name), name).initCause(exception);
}
try {
if (reference != null) {
param.setValue(reference);
} else if (unit != null) {
param.setValue(value, unit);
} else {
param.setValue(value);
}
} catch (RuntimeException exception) { // Catch InvalidParameterValueException, ArithmeticException and others.
throw new FactoryDataException(error().getString(Errors.Keys.CanNotSetParameterValue_1, name), exception);
}
}
}
}
/**
* Creates description of the algorithm and parameters used to perform a coordinate operation.
* An {@code OperationMethod} is a kind of metadata: it does not perform any coordinate operation
* (e.g. map projection) by itself, but tells us what is needed in order to perform such operation.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for operation methods are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>9804</td> <td>Mercator (variant A)</td></tr>
* <tr><td>9802</td> <td>Lambert Conic Conformal (2SP)</td></tr>
* <tr><td>9810</td> <td>Polar Stereographic (variant A)</td></tr>
* <tr><td>9624</td> <td>Affine parametric transformation</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the operation method for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized OperationMethod createOperationMethod(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
OperationMethod returnValue = null;
try (ResultSet result = executeQuery("Coordinate_Operation Method", "COORD_OP_METHOD_CODE", "COORD_OP_METHOD_NAME",
"SELECT COORD_OP_METHOD_CODE," +
" COORD_OP_METHOD_NAME," +
" REMARKS," +
" DEPRECATED" +
" FROM [Coordinate_Operation Method]" +
" WHERE COORD_OP_METHOD_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger (code, result, 1);
final String name = getString (code, result, 2);
final String remarks = getOptionalString (result, 3);
final boolean deprecated = getOptionalBoolean(result, 4);
final Integer[] dim = getDimensionsForMethod(epsg);
final ParameterDescriptor<?>[] descriptors = createParameterDescriptors(epsg);
Map<String,Object> properties = createProperties("Coordinate_Operation Method", name, epsg, remarks, deprecated);
// We do not store the formula at this time, because the text is very verbose and rarely used.
final OperationMethod method = new DefaultOperationMethod(properties, dim[0], dim[1],
new DefaultParameterDescriptorGroup(properties, 1, 1, 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;
}
/**
* Creates an operation for transforming coordinates in the source CRS to coordinates in the target CRS.
* The returned object will either be a {@link Conversion} or a {@link Transformation}, depending on the code.
*
* <div class="note"><b>Example:</b>
* some EPSG codes for coordinate transformations are:
*
* <table class="sis">
* <caption>EPSG codes examples</caption>
* <tr><th>Code</th> <th>Description</th></tr>
* <tr><td>1133</td> <td>ED50 to WGS 84 (1)</td></tr>
* <tr><td>1241</td> <td>NAD27 to NAD83 (1)</td></tr>
* <tr><td>1173</td> <td>NAD27 to WGS 84 (4)</td></tr>
* <tr><td>6326</td> <td>NAD83(2011) to NAVD88 height (1)</td></tr>
* </table></div>
*
* @param code value allocated by EPSG.
* @return the operation for the given code.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
@SuppressWarnings("null")
public synchronized CoordinateOperation createCoordinateOperation(final String code)
throws NoSuchAuthorityCodeException, FactoryException
{
ArgumentChecks.ensureNonNull("code", code);
CoordinateOperation returnValue = null;
try {
try (ResultSet result = executeQuery("Coordinate_Operation", "COORD_OP_CODE", "COORD_OP_NAME",
"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," +
" DEPRECATED" +
" FROM [Coordinate_Operation]" +
" WHERE COORD_OP_CODE = ?", code))
{
while (result.next()) {
final Integer epsg = getInteger(code, result, 1);
final String name = getString (code, result, 2);
final String type = getString (code, result, 3).toLowerCase(Locale.US);
final boolean isTransformation = type.equals("transformation");
final boolean isConversion = type.equals("conversion");
final boolean isConcatenated = type.equals("concatenated operation");
final String sourceCode, targetCode;
final Integer methodCode;
if (isConversion) {
sourceCode = getOptionalString(result, 4); // Optional for conversions, mandatory for all others.
targetCode = getOptionalString(result, 5);
} else {
sourceCode = getString(code, result, 4);
targetCode = getString(code, result, 5);
}
if (isConcatenated) {
methodCode = getOptionalInteger(result, 6); // Not applicable to concatenated operation, mandatory for all others.
} else {
methodCode = getInteger(code, result, 6);
}
final String version = getOptionalString (result, 7);
final double accuracy = getOptionalDouble (result, 8);
final String area = getOptionalString (result, 9);
final String scope = getOptionalString (result, 10);
final String remarks = getOptionalString (result, 11);
final boolean deprecated = getOptionalBoolean(result, 12);
/*
* Create the source and target CRS for the codes fetched above. Those CRS are optional only for
* conversions (the above calls to getString(code, result, …) verified that those CRS are defined
* for other kinds of operation). Conversions in EPSG database are usually "defining conversions"
* without source and target CRS.
*
* In EPSG database 6.7, all defining conversions are projections and their dimensions are always 2.
* However, this default number of dimensions is not generalizable to other kind of operation methods.
* For example the "Geocentric translation" operation method has 3-dimensional source and target CRS.
*/
boolean isDimensionKnown = true;
final int sourceDimensions, targetDimensions;
final CoordinateReferenceSystem sourceCRS, targetCRS;
if (sourceCode != null) {
sourceCRS = owner.createCoordinateReferenceSystem(sourceCode);
sourceDimensions = sourceCRS.getCoordinateSystem().getDimension();
} else {
sourceCRS = null;
sourceDimensions = 2; // Acceptable default for projections only.
isDimensionKnown = false;
}
if (targetCode != null) {
targetCRS = owner.createCoordinateReferenceSystem(targetCode);
targetDimensions = targetCRS.getCoordinateSystem().getDimension();
} else {
targetCRS = null;
targetDimensions = 2; // Acceptable default for projections only.
isDimensionKnown = false;
}
/*
* Get the operation method. This is mandatory for conversions and transformations
* (it was checked by getInteger(code, result, …) above in this method) but optional
* for concatenated operations. Fetching parameter values is part of this block.
*/
final boolean isDeferred = Semaphores.query(Semaphores.METADATA_ONLY);
ParameterValueGroup parameters = null;
OperationMethod method = null;
if (methodCode != null && !isDeferred) {
method = owner.createOperationMethod(methodCode.toString());
if (isDimensionKnown) {
method = DefaultOperationMethod.redimension(method, sourceDimensions, targetDimensions);
}
parameters = method.getParameters().createValue();
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 unconditionally if present.
*
* NOTE: This block must be executed last before object creations below, because
* methods like createCoordinateReferenceSystem and createOperationMethod
* overwrite the properties map.
*/
Map<String,Object> opProperties = createProperties("Coordinate_Operation",
name, epsg, area, scope, remarks, deprecated);
opProperties.put(CoordinateOperation.OPERATION_VERSION_KEY, version);
if (!Double.isNaN(accuracy)) {
opProperties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
TransformationAccuracy.create(accuracy));
}
/*
* 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).
*/
final CoordinateOperation operation;
final CoordinateOperationFactory copFactory = owner.copFactory;
if (isDeferred) {
operation = new DeferredCoordinateOperation(opProperties, sourceCRS, targetCRS, owner);
} else if (isConversion && (sourceCRS == null || targetCRS == null)) {
operation = copFactory.createDefiningConversion(opProperties, 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.
*/
result.close();
opProperties = new HashMap<>(opProperties); // Because this class uses a shared map.
final List<String> codes = new ArrayList<>();
try (ResultSet cr = executeQuery("Coordinate_Operation Path",
"SELECT SINGLE_OPERATION_CODE" +
" FROM [Coordinate_Operation Path]" +
" WHERE (CONCAT_OPERATION_CODE = ?)" +
" ORDER BY OP_PATH_STEP", epsg))
{
while (cr.next()) {
codes.add(getString(code, cr, 1));
}
}
final CoordinateOperation[] operations = new CoordinateOperation[codes.size()];
ensureNoCycle(CoordinateOperation.class, epsg);
try {
for (int i=0; i<operations.length; i++) {
operations[i] = owner.createCoordinateOperation(codes.get(i));
}
} finally {
endOfRecursivity(CoordinateOperation.class, epsg);
}
return copFactory.createConcatenatedOperation(opProperties, operations);
} else {
/*
* At this stage, the parameters are ready for use. Create the math transform and wrap it in the
* final operation (a Conversion or a Transformation). We need to give to MathTransformFactory
* some information about the context (source and target CRS) for allowing the factory to set
* the values of above-mentioned implicit parameters (semi-major and semi-minor axis lengths).
*
* The first special case may be removed in a future SIS version if the missing method is added
* to GeoAPI. Actually GeoAPI has a method doing part of the job, but incomplete (e.g. the pure
* GeoAPI method can not handle Molodensky transform because it does not give the target datum).
*/
final MathTransform mt;
final MathTransformFactory mtFactory = owner.mtFactory;
if (mtFactory instanceof DefaultMathTransformFactory) {
mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform(parameters,
ReferencingUtilities.createTransformContext(sourceCRS, targetCRS, null));
} else {
// Fallback for non-SIS implementations. Work for map projections but not for Molodensky.
mt = mtFactory.createBaseToDerived(sourceCRS, parameters, targetCRS.getCoordinateSystem());
}
/*
* Give a hint to the factory about the type of the coordinate operation. ISO 19111 defines
* Conversion and Transformation, but SIS also have more specific sub-types. We begin with
* what we can infer from the EPSG database. Next, if the SIS MathTransform providers give
* more information, then we refine the type.
*/
Class<? extends SingleOperation> opType;
if (isTransformation) {
opType = Transformation.class;
} else if (isConversion) {
opType = Conversion.class;
} else {
opType = SingleOperation.class;
}
final OperationMethod provider = mtFactory.getLastMethodUsed();
if (provider instanceof DefaultOperationMethod) { // SIS-specific
final Class<?> s = ((DefaultOperationMethod) provider).getOperationType();
if (s != null && opType.isAssignableFrom(s)) {
opType = s.asSubclass(SingleOperation.class);
}
}
opProperties.put(CoordinateOperations.OPERATION_TYPE_KEY, opType);
opProperties.put(CoordinateOperations.PARAMETERS_KEY, parameters);
/*
* Following restriction will be removed in a future SIS version if the method is added to GeoAPI.
*/
if (!(copFactory instanceof DefaultCoordinateOperationFactory)) {
throw new UnsupportedOperationException(error().getString(
Errors.Keys.UnsupportedImplementation_1, copFactory.getClass()));
}
operation = ((DefaultCoordinateOperationFactory) copFactory)
.createSingleOperation(opProperties, sourceCRS, targetCRS, null, method, mt);
}
returnValue = ensureSingleton(operation, returnValue, code);
if (result.isClosed()) {
return returnValue;
}
}
}
} catch (SQLException exception) {
throw databaseFailure(CoordinateOperation.class, code, exception);
}
if (returnValue == null) {
throw noSuchAuthorityCode(CoordinateOperation.class, code);
}
return returnValue;
}
/**
* Creates operations from source and target coordinate reference system codes.
* This method only extract the information explicitly declared in the EPSG database;
* it does not attempt to infer by itself operations that are not explicitly recorded in the database.
*
* <p>The returned set is ordered with the most accurate operations first.
* Deprecated operations are not included in the set; if a deprecated operation is really wanted,
* it can be fetched by an explicit call to {@link #createCoordinateOperation(String)}.</p>
*
* @param sourceCRS coded value of source coordinate reference system.
* @param targetCRS coded value of target coordinate reference system.
* @return the operations from {@code sourceCRS} to {@code targetCRS}.
* @throws NoSuchAuthorityCodeException if a specified code was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
@Override
public synchronized Set<CoordinateOperation> createFromCoordinateReferenceSystemCodes(
final String sourceCRS, final String targetCRS) throws FactoryException
{
ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
final String label = sourceCRS + " ⇨ " + targetCRS;
final CoordinateOperationSet set = new CoordinateOperationSet(owner);
try {
final int[] pair = toPrimaryKeys(null, null, null, sourceCRS, targetCRS);
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] AS CO" +
" JOIN [Area] ON AREA_OF_USE_CODE = AREA_CODE" +
" WHERE CO.DEPRECATED=0" + // Do not put spaces around "=" - SQLTranslator searches for this exact match.
" AND SOURCE_CRS_CODE = ?" +
" AND TARGET_CRS_CODE = ?" +
" ORDER BY COORD_OP_ACCURACY ASC NULLS LAST, " +
" (AREA_EAST_BOUND_LON - AREA_WEST_BOUND_LON + CASE WHEN AREA_EAST_BOUND_LON < AREA_WEST_BOUND_LON THEN 360 ELSE 0 END)" +
" * (AREA_NORTH_BOUND_LAT - AREA_SOUTH_BOUND_LAT)" +
" * COS(RADIANS(AREA_NORTH_BOUND_LAT + AREA_SOUTH_BOUND_LAT)/2) DESC";
} else {
key = "ConversionFromCRS";
sql = "SELECT PROJECTION_CONV_CODE" +
" FROM [Coordinate Reference System]" +
" WHERE SOURCE_GEOGCRS_CODE = ?" +
" AND COORD_REF_SYS_CODE = ?";
}
final Integer targetKey = searchTransformations ? null : pair[1];
try (ResultSet result = executeQuery(key, sql, pair)) {
while (result.next()) {
set.addAuthorityCode(getString(label, result, 1), targetKey);
}
}
} 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();
if (codes.length > 1 && sort("Coordinate_Operation", codes)) {
set.setAuthorityCodes(codes);
}
} catch (SQLException exception) {
throw databaseFailure(CoordinateOperation.class, label, 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.
*/
if (!Semaphores.query(Semaphores.METADATA_ONLY)) {
set.resolve(1);
}
return set;
}
/**
* Returns a finder which can be used for looking up unidentified objects.
* The finder tries to fetch a fully {@linkplain AbstractIdentifiedObject identified object} from an incomplete one,
* for example from an object without "{@code ID[…]}" or "{@code AUTHORITY[…]}" element in <cite>Well Known Text</cite>.
*
* @return a finder to use for looking up unidentified objects.
* @throws FactoryException if the finder can not be created.
*/
@Override
public IdentifiedObjectFinder newIdentifiedObjectFinder() throws FactoryException {
return new EPSGCodeFinder(this);
}
/**
* Returns {@code true} if the {@link CoordinateOperation} for the specified code is a {@link Projection}.
* The caller must have verified that the designed operation is a {@link Conversion} before to invoke this method.
*
* @throws SQLException if an error occurred while querying the database.
*/
final boolean isProjection(final Integer code) throws SQLException {
Boolean projection = isProjection.get(code);
if (projection == null) {
try (ResultSet result = executeQuery("isProjection",
"SELECT COORD_REF_SYS_CODE" +
" FROM [Coordinate Reference System]" +
" WHERE PROJECTION_CONV_CODE = ?" +
" AND CAST(COORD_REF_SYS_KIND AS " + TableInfo.ENUM_REPLACEMENT + ") LIKE 'projected%'", code))
{
projection = result.next();
}
isProjection.put(code, projection);
}
return projection;
}
/**
* Returns the source and target dimensions for the specified method, provided that they are the same
* for all operations using that method. The returned array has a length of 2 and is never null,
* but some elements in that array may be null.
*
* @param method the EPSG code of the operation method for which to get the dimensions.
* @return the dimensions in an array of length 2.
*
* @see #getDimensionForCS(Integer)
*/
private Integer[] getDimensionsForMethod(final Integer method) throws SQLException {
final Integer[] dimensions = new Integer[2];
final boolean[] differents = new boolean[2];
int numDifferences = 0;
boolean projections = false;
do {
/*
* This loop is executed twice. On the first execution, we look for the source and
* target CRS declared directly in the "Coordinate Operations" table. This applies
* mostly to coordinate transformations, since those fields are typically empty in
* the case of projected CRS.
*
* In the second execution, we will look for the base geographic CRS and
* the resulting projected CRS that use the given operation method. This
* allows us to handle the case of projected CRS (typically 2 dimensional).
*/
final String key, sql;
if (!projections) {
key = "MethodDimensions";
sql = "SELECT DISTINCT SRC.COORD_SYS_CODE," +
" TGT.COORD_SYS_CODE" +
" FROM [Coordinate_Operation] AS CO" +
" INNER JOIN [Coordinate Reference System] AS SRC ON SRC.COORD_REF_SYS_CODE = CO.SOURCE_CRS_CODE" +
" INNER JOIN [Coordinate Reference System] AS TGT ON TGT.COORD_REF_SYS_CODE = CO.TARGET_CRS_CODE" +
" WHERE CO.DEPRECATED=0 AND COORD_OP_METHOD_CODE = ?";
// Do not put spaces in "DEPRECATED=0" - SQLTranslator searches for this exact match.
} else {
key = "DerivedDimensions";
sql = "SELECT DISTINCT SRC.COORD_SYS_CODE," +
" TGT.COORD_SYS_CODE" +
" FROM [Coordinate Reference System] AS TGT" +
" INNER JOIN [Coordinate Reference System] AS SRC ON TGT.SOURCE_GEOGCRS_CODE = SRC.COORD_REF_SYS_CODE" +
" INNER JOIN [Coordinate_Operation] AS CO ON TGT.PROJECTION_CONV_CODE = CO.COORD_OP_CODE" +
" WHERE CO.DEPRECATED=0 AND COORD_OP_METHOD_CODE = ?";
}
try (ResultSet result = executeQuery(key, sql, method)) {
while (result.next()) {
for (int i=0; i<dimensions.length; i++) {
if (!differents[i]) { // Not worth to test heterogenous dimensions.
final Integer dim = getDimensionForCS(result.getInt(i + 1));
if (dim != null) {
if (dimensions[i] == null) {
dimensions[i] = dim;
} else if (!dim.equals(dimensions[i])) {
dimensions[i] = null;
differents[i] = true;
if (++numDifferences == differents.length) {
// All dimensions have been set to null.
return dimensions;
}
}
}
}
}
}
}
} while ((projections = !projections) == true);
return dimensions;
}
/**
* 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 tries
* to preserve the old ordering of the supplied codes (since deprecated operations should already be last).
* The ordering is performed in place.
*
* @param table the table of the objects for which to check for supersession.
* @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 return the code for each element.
* @return {@code true} if the array changed as a result of this method call.
*/
final synchronized boolean sort(final String table, final Object[] codes) throws SQLException, FactoryException {
int iteration = 0;
do {
boolean changed = false;
for (int i=0; i<codes.length; i++) {
final int code;
try {
code = Integer.parseInt(codes[i].toString());
} catch (NumberFormatException e) {
unexpectedException("sort", e);
continue;
}
// Note: "OBJECT_TABLE_NAME=?" is handled in a special way by SQLTranslator.
try (ResultSet result = executeMetadataQuery("Supersession",
"SELECT SUPERSEDED_BY FROM [Supersession]" +
" WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?" +
" ORDER BY SUPERSESSION_YEAR DESC", table, code))
{
while (result.next()) {
final String replacement = result.getString(1);
if (replacement != null) {
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;
}
}
}
}
}
}
if (!changed) {
return iteration != 0;
}
}
while (++iteration < Formulas.MAXIMUM_ITERATIONS); // Arbitrary limit for avoiding never-ending loop.
return true;
}
/**
* Creates an exception for an unknown authority code.
* This convenience method is provided for implementation of {@code createFoo(String)} methods.
*
* @param type the GeoAPI interface that was to be created (e.g. {@code CoordinateReferenceSystem.class}).
* @param code the unknown authority code.
* @return an exception initialized with an error message built from the specified information.
*/
private NoSuchAuthorityCodeException noSuchAuthorityCode(final Class<?> type, final String code) {
return new NoSuchAuthorityCodeException(resources().getString(Resources.Keys.NoSuchAuthorityCode_3,
Constants.EPSG, type, code), Constants.EPSG, code, code);
}
/**
* Constructs an exception for a database failure.
*/
final FactoryException databaseFailure(Class<?> type, Comparable<?> code, SQLException cause) {
return new FactoryException(error().getString(Errors.Keys.DatabaseError_2, type, code), cause);
}
/**
* Minor shortcut for fetching the error resources.
*/
private Errors error() {
return Errors.getResources(getLocale());
}
/**
* Minor shortcut for fetching the resources specific to the {@code sis-referencing} module.
*/
private Resources resources() {
return Resources.forLocale(getLocale());
}
/**
* Logs a warning about an unexpected but non-fatal exception.
*
* @param method the source method.
* @param exception the exception to log.
*/
private static void unexpectedException(final String method, final Exception exception) {
Logging.unexpectedException(Logging.getLogger(Loggers.CRS_FACTORY), EPSGDataAccess.class, method, exception);
}
/**
* Returns {@code true} if it is safe to close this factory. This method is invoked indirectly
* by {@link EPSGFactory} after some timeout in order to release resources.
* This method will block the disposal if some {@link AuthorityCodes} are still in use.
*/
final synchronized boolean canClose() {
boolean can = true;
if (!authorityCodes.isEmpty()) {
System.gc(); // For cleaning as much weak references as we can before we check them.
final Iterator<CloseableReference<AuthorityCodes>> it = authorityCodes.values().iterator();
while (it.hasNext()) {
final AuthorityCodes codes = it.next().get();
if (codes == null) {
it.remove();
} else {
/*
* A set of authority codes is still in use. We can not close this factory.
* But we continue the iteration anyway in order to cleanup weak references.
*/
can = false;
}
}
}
return can;
}
/**
* Closes the JDBC connection used by this factory.
* If this {@code EPSGDataAccess} is used by an {@link EPSGFactory}, then this method
* will be automatically invoked after some {@linkplain EPSGFactory#getTimeout timeout}.
*
* @throws FactoryException if an error occurred while closing the connection.
*
* @see #connection
*/
@Override
public synchronized void close() throws FactoryException {
SQLException exception = null;
final Iterator<PreparedStatement> ip = statements.values().iterator();
while (ip.hasNext()) {
try {
ip.next().close();
} catch (SQLException e) {
if (exception == null) {
exception = e;
} else {
exception.addSuppressed(e);
}
}
ip.remove();
}
final Iterator<CloseableReference<AuthorityCodes>> it = authorityCodes.values().iterator();
while (it.hasNext()) {
try {
it.next().close();
} catch (SQLException e) {
if (exception == null) {
exception = e;
} else {
exception.addSuppressed(e);
}
}
it.remove();
}
try {
connection.close();
} catch (SQLException e) {
if (exception == null) {
exception = e;
} else {
e.addSuppressed(exception); // Keep the connection thrown be Connection as the main one to report.
}
}
if (exception != null) {
throw new FactoryException(exception);
}
}
}