/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.referencing.factory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.measure.unit.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.FactoryException;
import org.opengis.util.GenericName;
import org.opengis.util.ScopedName;
import org.geotools.util.LocalName;
import org.geotools.util.NameFactory;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Loggings;
import org.geotools.resources.i18n.LoggingKeys;
import org.geotools.referencing.ReferencingFactoryFinder;
/**
* A datum factory that add {@linkplain IdentifiedObject#getAlias aliases} to a datum name before to
* delegates the {@linkplain org.geotools.referencing.datum.AbstractDatum#AbstractDatum(Map) datum
* creation} to an other factory. Aliases are especially important for {@linkplain Datum datum}
* since their {@linkplain IdentifiedObject#getName name} are often the only way to differentiate
* them. Two datum with different names are considered incompatible, unless some datum shift method
* are specified (e.g. {@linkplain org.geotools.referencing.datum.BursaWolfParameters Bursa-Wolf
* parameters}). Unfortunatly, different softwares often use different names for the same datum,
* which result in {@link org.opengis.referencing.operation.OperationNotFoundException} when
* attempting to convert coordinates from one {@linkplain CoordinateReferenceSystem coordinate
* reference system} to an other one. For example "<cite>Nouvelle Triangulation Française (Paris)</cite>"
* and "<cite>NTF (Paris meridian)</cite>" are actually the same datum. This {@code DatumAliases}
* class provides a way to handle that.
* <p>
* {@code DatumAliases} is a class that determines if a datum name is in our list of aliases and
* constructs a value for the {@linkplain IdentifiedObject#ALIAS_KEY aliases property} (as
* {@linkplain GenericName generic names}) for a name. The default implementation is backed by
* the text file "{@code DatumAliasesTable.txt}". The first line in this text file must be the
* authority names. All other lines are the aliases.
* <p>
* Since {@code DatumAliases} is a datum factory, any {@linkplain AuthorityFactory authority
* factory} or any {@linkplain org.geotools.referencing.wkt.Parser WKT parser} using this
* factory will takes advantage of the aliases table.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Rueben Schulz
* @author Martin Desruisseaux
*
* @todo Invokes {@link #freeUnused} automatically after some amount of time, in order to release
* memory for unusued aliases. A timer should be set in {@code reload()} method.
*
* @see <A HREF="http://gdal.velocet.ca/~warmerda/wktproblems.html">WKT problems</A>
*/
public class DatumAliases extends ReferencingFactory implements DatumFactory {
/**
* The default file for alias table.
*/
private static final String ALIAS_TABLE = "DatumAliasesTable.txt";
/**
* The column separators in the file to parse.
*/
private static final String SEPARATORS = ";";
/**
* Array used as a marker for alias that has been discarted because never used.
* This array may appears in {@link #aliasMap} values.
*
* @see #freeUnused
*/
private static final Object[] NEED_LOADING = new Object[0];
/**
* The URL of the alias table. This file is read by {@link #reload} when first needed.
*/
private final URL aliasURL;
/**
* A map of our datum aliases. Keys are alias names in lower-case, and values are
* either {@code String[]} or {@code GenericName[]}. In order to reduce the amount
* of objects created, all values are initially {@code String[]} objects. They are
* converted to {@code GenericName[]} only when first needed.
*/
private final Map<String,Object[]> aliasMap = new HashMap<String,Object[]>();
/**
* The authorities. This is the first line in the alias table.
* This array is constructed by {@link #reload} when first needed.
*/
private LocalName[] authorities;
/**
* The underlying datum factory. If {@code null}, a default factory will be fetch from
* {@link ReferencingFactoryFinder} when first needed. A default value can't be set at
* construction time, since all factories may not be registered at this time.
*/
private DatumFactory factory;
/**
* Constructs a new datum factory with the default backing factory and alias table.
*/
public DatumAliases() {
// Uses a slightly higher priority than the default factory, in order
// to get WKT parser and authorities factories to use the aliases table.
super(NORMAL_PRIORITY + 10);
aliasURL = DatumAliases.class.getResource(ALIAS_TABLE);
if (aliasURL == null) {
throw new NoSuchElementException(ALIAS_TABLE);
}
}
/**
* Constructs a new datum factory using the specified factory and the default alias table.
*
* @param factory The factory to use for datum creation.
*/
public DatumAliases(final DatumFactory factory) {
this();
this.factory = factory;
ensureNonNull("factory", factory);
}
/**
* Constructs a new datum factory which delegates its work to the specified factory.
* The aliases table is read from the specified URL. The fist line in this file most
* be the authority names. All other names are aliases.
*
* @param factory The factory to use for datum creation.
* @param aliasURL The url to the alias table.
*/
public DatumAliases(final DatumFactory factory, final URL aliasURL) {
super(NORMAL_PRIORITY + 10);
this.factory = factory;
this.aliasURL = aliasURL;
ensureNonNull("factory", factory );
ensureNonNull("aliasURL", aliasURL);
}
/**
* Returns the backing datum factory. If no factory were explicitly specified
* by the user, selects the first datum factory other than {@code this}.
* <p>
* <strong>Note:</strong> We can't invoke this method in the constructor, because the
* constructor is typically invoked during {@code FactoryFinder.scanForPlugins()} execution.
* {@code scanForPlugins} is looking for {@link DatumFactory} instances, it has not finished
* to search them, and invoking this method in the constructor would prematurely ask an other
* {@link DatumFactory} instance while the list is incomplete. Instead, we will invoke this
* method when the first {@code createXXX} method is invoked, which typically occurs after
* all factories have been initialized.
*
* @return The backing datum factory.
* @throws NoSuchElementException if there is no such factory.
*/
private DatumFactory getDatumFactory() throws NoSuchElementException {
assert Thread.holdsLock(this);
if (factory == null) {
DatumFactory candidate;
final Iterator<DatumFactory> it = ReferencingFactoryFinder.getDatumFactories(null).iterator();
do candidate = it.next();
while (candidate == this);
factory = candidate;
}
return factory;
}
/**
* Returns a caseless version of the specified key, to be stored in the map.
*/
private static String toCaseless(final String key) {
return key.replace('_', ' ').trim().toLowerCase();
}
/**
* Read the next line from the specified input stream, skipping all blank
* and comment lines. Returns {@code null} on end of stream.
*/
private static String readLine(final BufferedReader in) throws IOException {
String line;
do line = in.readLine();
while (line!=null && ((line=line.trim()).length()==0 || line.charAt(0)=='#'));
return line;
}
/**
* Read again the "{@code DatumAliasesTable.txt}" file into {@link #aliasMap}.
* This method may be invoked more than once in order to reload entries that
* have been discarted by {@link #freeUnused}. This method assumes that the
* file content didn't change between two calls.
*
* @throws IOException if the loading failed.
*/
private void reload() throws IOException {
assert Thread.holdsLock(this);
final LogRecord record = Loggings.format(Level.FINE,
LoggingKeys.LOADING_DATUM_ALIASES_$1, aliasURL);
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
final BufferedReader in = new BufferedReader(new InputStreamReader(aliasURL.openStream()));
/*
* Parses the title line. This line contains authority names as column titles.
* The authority names will be used as the scope for each identifiers to be
* created.
*/
String line = readLine(in);
if (line != null) {
final List<Object> elements = new ArrayList<Object>();
StringTokenizer st = new StringTokenizer(line, SEPARATORS);
while (st.hasMoreTokens()) {
final String name = st.nextToken().trim();
elements.add(name.length()!=0 ? new LocalName(name) : null);
}
authorities = elements.toArray(new LocalName[elements.size()]);
final Map<String,String> canonical = new HashMap<String,String>();
/*
* Parses all aliases. They are stored as arrays of strings for now, but will be
* converted to array of generic names by {@link #getAliases} when first needed.
* If the alias belong to an authority (which should be true in most cases), a
* scoped name will be created at this time.
*/
while ((line=readLine(in)) != null) {
elements .clear();
canonical.clear();
st = new StringTokenizer(line, SEPARATORS);
while (st.hasMoreTokens()) {
String alias = st.nextToken().trim();
if (alias.length() != 0) {
final String previous = canonical.put(alias, alias);
if (previous != null) {
canonical.put(previous, previous);
alias = previous;
}
} else {
alias = null;
}
elements.add(alias);
}
// Trim trailing null values only (we must keep other null values).
for (int i=elements.size(); --i>=0;) {
if (elements.get(i) != null) break;
elements.remove(i);
}
if (!elements.isEmpty()) {
/*
* Copies the aliases array in the aliases map for all local names. If a
* previous value is found as an array of GenericName objects, those generic
* names are conserved in the map (instead of the string values parsed above)
* in order to avoid constructing them again when they will be needed.
*/
final String[] names = elements.toArray(new String[elements.size()]);
for (int i=0; i<names.length; i++) {
final String name = names[i];
final String key = toCaseless(name);
final Object[] previous = aliasMap.put(key, names);
if (previous!=null && previous!=NEED_LOADING) {
if (previous instanceof GenericName[]) {
aliasMap.put(key, previous);
} else if (!Arrays.equals(previous, names)) {
// TODO: localize
LOGGER.warning("Inconsistent aliases for datum \""+name+"\".");
}
}
}
}
}
}
in.close();
}
/**
* Logs an {@link IOException}.
*/
private void log(final IOException exception) {
LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.CANT_READ_FILE_$1, aliasURL);
record.setSourceClassName(DatumAliases.class.getName());
record.setSourceMethodName("reload");
record.setThrown(exception);
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
}
/**
* Returns the aliases, as a set of {@link GenericName}, for the given name.
* This method returns an internal array; do not modify the returned value.
*
* @param name Datum alias name to lookup.
* @return A set of datum aliases as {@link GenericName} objects for the given name,
* or {@code null} if the name is not in our list of aliases.
*
* @see #addAliases
* @see #reload
*/
private GenericName[] getAliases(String name) {
assert Thread.holdsLock(this);
if (aliasMap.isEmpty()) try {
reload();
} catch (IOException exception) {
log(exception);
// Continue in case the requested alias has been read before the failure occured.
}
/*
* Gets the aliases for the specified name. If an entry exists for this name with a null
* value, this means that 'freeUnused()' has been invoked previously. Reload the file and
* try again since the requested name may be one of the set of discarted aliases.
*/
name = toCaseless(name);
Object[] aliases = aliasMap.get(name);
if (aliases == null) {
// Unknow name. We are done.
return null;
}
if (aliases == NEED_LOADING) {
// Known name, but the list of alias has been previously
// discarted because never used. Reload the file.
try {
reload();
} catch (IOException exception) {
log(exception);
// Continue in case the requested alias has been read before the failure occured.
}
aliases = aliasMap.get(name);
if (aliases == NEED_LOADING) {
// Should never happen, unless reloading failed or some lines have
// been deleted in the file since last time the file has been loaded.
return null;
}
}
if (aliases instanceof GenericName[]) {
return (GenericName[]) aliases;
}
/*
* Aliases has been found, but available as an array of strings only. This means
* that those aliases have never been requested before. Transforms the array of
* strings into an array of generic names. The new array replaces the old one for
* all aliases enumerated in the array (not just the requested one).
*/
int count = 0;
GenericName[] names = new GenericName[aliases.length];
for (int i=0; i<aliases.length; i++) {
final CharSequence alias = (CharSequence) aliases[i];
if (alias != null) {
if (count < authorities.length) {
final LocalName authority = authorities[count];
if (authority != null) {
names[count++] = new org.geotools.util.ScopedName(authority, alias);
continue;
}
}
names[count++] = new LocalName(alias);
}
}
names = XArray.resize(names, count);
for (int i=0; i<names.length; i++) {
final String alias = names[i].tip().toString();
final Object[] previous = aliasMap.put(toCaseless(alias), names);
assert previous==names || Arrays.equals(aliases, previous) : alias;
}
return names;
}
/**
* Completes the given map of properties. This method expects a map of properties to
* be given to {@link AbstractDatum#AbstractDatum(Map)} constructor. The name is fetch
* from the {@link IdentifiedObject#NAME_KEY NAME_KEY}.
* The {@link AbstractIdentifiedObject#ALIAS_KEY ALIAS_KEY} is
* completed with the aliases know to this factory.
*
* @param properties The set of properties to complete.
* @return The completed properties, or {@code properties} if no change were done.
*
* @see #getAliases
*/
private Map<String,?> addAliases(Map<String,?> properties) {
ensureNonNull("properties", properties);
Object value = properties.get(IdentifiedObject.NAME_KEY);
ensureNonNull("name", value);
final String name;
if (value instanceof Identifier) {
name = ((Identifier) value).getCode();
} else {
name = value.toString();
}
GenericName[] aliases = getAliases(name);
if (aliases != null) {
/*
* Aliases have been found. Before to add them to the properties map, overrides them
* with the aliases already provided by the users, if any. The 'merged' map is the
* union of aliases know to this factory and aliases provided by the user. User's
* aliases will be added first, for preserving the user's order (the LinkedHashMap
* acts as a FIFO queue).
*/
int count = aliases.length;
value = properties.get(IdentifiedObject.ALIAS_KEY);
if (value != null) {
final Map<String,GenericName> merged = new LinkedHashMap<String,GenericName>();
putAll(NameFactory.toArray(value), merged);
count -= putAll(aliases, merged);
final Collection<GenericName> c = merged.values();
aliases = c.toArray(new GenericName[c.size()]);
}
/*
* Now set the aliases. This replacement will not be performed if
* all our aliases were replaced by user's aliases (count <= 0).
*/
if (count > 0) {
final Map<String,Object> copy = new HashMap<String,Object>(properties);
copy.put(IdentifiedObject.ALIAS_KEY, aliases);
properties = copy;
}
}
return properties;
}
/**
* Puts all elements in the {@code names} array into the specified map. Order matter, since the
* first element in the array should be the first element returned by the map if the map is
* actually an instance of {@link LinkedHashMap}. This method returns the number of elements
* ignored.
*/
private static final int putAll(final GenericName[] names, final Map<String,GenericName> map) {
int ignored = 0;
for (int i=0; i<names.length; i++) {
final GenericName name = names[i];
final GenericName scoped = name.toFullyQualifiedName();
final String key = toCaseless(scoped.toString());
final GenericName old = map.put(key, name);
if (old instanceof ScopedName) {
map.put(key, old); // Preserves the user value, except if it was unscoped.
ignored++;
}
}
return ignored;
}
/**
* Creates an engineering datum.
*
* @param properties Name and other properties to give to the new object.
* @throws FactoryException if the object creation failed.
*/
public synchronized EngineeringDatum createEngineeringDatum(final Map<String,?> properties)
throws FactoryException
{
return getDatumFactory().createEngineeringDatum(addAliases(properties));
}
/**
* Creates geodetic datum from ellipsoid and (optionaly) Bursa-Wolf parameters.
*
* @param properties Name and other properties to give to the new object.
* @param ellipsoid Ellipsoid to use in new geodetic datum.
* @param primeMeridian Prime meridian to use in new geodetic datum.
* @throws FactoryException if the object creation failed.
*/
public synchronized GeodeticDatum createGeodeticDatum(final Map<String,?> properties,
final Ellipsoid ellipsoid, final PrimeMeridian primeMeridian) throws FactoryException
{
return getDatumFactory().createGeodeticDatum(addAliases(properties),
ellipsoid, primeMeridian);
}
/**
* Creates an image datum.
*
* @param properties Name and other properties to give to the new object.
* @param pixelInCell Specification of the way the image grid is associated
* with the image data attributes.
* @throws FactoryException if the object creation failed.
*/
public synchronized ImageDatum createImageDatum(final Map<String,?> properties,
final PixelInCell pixelInCell) throws FactoryException
{
return getDatumFactory().createImageDatum(addAliases(properties), pixelInCell);
}
/**
* Creates a temporal datum from an enumerated type value.
*
* @param properties Name and other properties to give to the new object.
* @param origin The date and time origin of this temporal datum.
* @throws FactoryException if the object creation failed.
*/
public synchronized TemporalDatum createTemporalDatum(final Map<String,?> properties,
final Date origin) throws FactoryException
{
return getDatumFactory().createTemporalDatum(addAliases(properties), origin);
}
/**
* Creates a vertical datum from an enumerated type value.
*
* @param properties Name and other properties to give to the new object.
* @param type The type of this vertical datum (often geoidal).
* @throws FactoryException if the object creation failed.
*/
public synchronized VerticalDatum createVerticalDatum(final Map<String,?> properties,
final VerticalDatumType type) throws FactoryException
{
return getDatumFactory().createVerticalDatum(addAliases(properties), type);
}
/**
* Creates an ellipsoid from radius values.
*
* @param properties Name and other properties to give to the new object.
* @param semiMajorAxis Equatorial radius in supplied linear units.
* @param semiMinorAxis Polar radius in supplied linear units.
* @param unit Linear units of ellipsoid axes.
* @throws FactoryException if the object creation failed.
*/
public synchronized Ellipsoid createEllipsoid(final Map<String,?> properties,
final double semiMajorAxis, final double semiMinorAxis, final Unit<Length> unit)
throws FactoryException
{
return getDatumFactory().createEllipsoid(addAliases(properties),
semiMajorAxis, semiMinorAxis, unit);
}
/**
* Creates an ellipsoid from an major radius, and inverse flattening.
*
* @param properties Name and other properties to give to the new object.
* @param semiMajorAxis Equatorial radius in supplied linear units.
* @param inverseFlattening Eccentricity of ellipsoid.
* @param unit Linear units of major axis.
* @throws FactoryException if the object creation failed.
*/
public synchronized Ellipsoid createFlattenedSphere(final Map<String,?> properties,
final double semiMajorAxis, final double inverseFlattening, final Unit<Length> unit)
throws FactoryException
{
return getDatumFactory().createFlattenedSphere(addAliases(properties),
semiMajorAxis, inverseFlattening, unit);
}
/**
* Creates a prime meridian, relative to Greenwich.
*
* @param properties Name and other properties to give to the new object.
* @param longitude Longitude of prime meridian in supplied angular units East of Greenwich.
* @param angularUnit Angular units of longitude.
* @throws FactoryException if the object creation failed.
*/
public synchronized PrimeMeridian createPrimeMeridian(final Map<String,?> properties,
final double longitude, final Unit<Angle> angularUnit) throws FactoryException
{
return getDatumFactory().createPrimeMeridian(addAliases(properties),
longitude, angularUnit);
}
/**
* Free all aliases that have been unused up to date. If one of those alias is needed at a
* later time, the aliases table will be reloaded.
*/
public synchronized void freeUnused() {
if (aliasMap != null) {
for (final Map.Entry<String,Object[]> entry : aliasMap.entrySet()) {
final Object[] value = entry.getValue();
if (!(value instanceof GenericName[])) {
entry.setValue(NEED_LOADING);
}
}
}
}
}