org.geotoolkit.referencing.factory.DatumAliases Maven / Gradle / Ivy
/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.referencing.factory;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.net.URL;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.measure.unit.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import net.jcip.annotations.ThreadSafe;
import org.opengis.util.ScopedName;
import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.IdentifiedObject;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.naming.DefaultNameFactory;
import org.geotoolkit.referencing.NamedIdentifier;
import org.geotoolkit.metadata.iso.citation.Citations;
import org.geotoolkit.resources.Loggings;
import org.geotoolkit.internal.Threads;
import org.geotoolkit.util.XArrays;
/**
* A datum factory that add {@linkplain IdentifiedObject#getAlias aliases} to a datum name before to
* delegates the {@linkplain org.geotoolkit.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.geotoolkit.referencing.datum.BursaWolfParameters Bursa-Wolf
* parameters}). Unfortunately, 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 org.opengis.referencing.crs.CoordinateReferenceSystem coordinate reference system}
* to an other one. For example "Nouvelle Triangulation Française (Paris)" and
* "NTF (Paris meridian)" are actually the same datum. This {@code DatumAliases}
* class provides a way to handle that.
*
* {@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}). The default implementation is backed by the
* "{@code DatumAliasesTable.csv}" text file. The first uncommented non-blank line in this
* text file must be the authority names. All other lines are the aliases.
*
* Since {@code DatumAliases} is a datum factory, any
* {@linkplain org.opengis.referencing.AuthorityFactory authority factory} or any
* {@linkplain org.geotoolkit.io.wkt.ReferencingParser WKT parser} using this factory
* will takes advantage of the aliases table.
*
* @author Rueben Schulz (UBC)
* @author Martin Desruisseaux (Geomatys, IRD)
* @version 3.19
*
* @see WKT problems
*
* @since 2.1
* @module
*/
@ThreadSafe
public class DatumAliases extends ReferencingFactory implements DatumFactory {
/**
* The default file for alias table.
*/
private static final String ALIAS_TABLE = "DatumAliasesTable.csv";
/**
* The column separators in the file to parse.
*/
private static final String SEPARATORS = ";";
/**
* Array used as a marker for alias that has been discarded 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 aliasMap = new HashMap();
/**
* The authorities. This is the first line in the alias table.
* This array is constructed by {@link #reload()} when first needed.
*/
private Citation[] authorities;
/**
* The factory used for creating names. We do not allow this factory to be set by
* user-hints because it would be misleading: must of the name creation is actually
* performed by {@link NamedIdentifier}, which fetches its factory instance itself.
*/
private transient DefaultNameFactory nameFactory;
/**
* The underlying datum factory. If {@code null}, a default factory will be fetch from
* {@link FactoryFinder} 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 datumFactory;
/**
* Constructs a new datum factory with the default backing factory and alias table.
*/
public DatumAliases() {
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();
datumFactory = 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 uncommented non-blank
* 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) {
ensureNonNull("factory", factory );
ensureNonNull("aliasURL", aliasURL);
this.aliasURL = aliasURL;
datumFactory = factory;
}
/**
* Invoked by {@link org.geotoolkit.factory.FactoryRegistry} in order to set the ordering relative
* to other factories. The current implementation specifies that this factory should have
* precedence over {@link ReferencingObjectFactory}.
*
* @since 3.00
*/
@Override
protected void setOrdering(final Organizer organizer) {
super.setOrdering(organizer);
organizer.before(ReferencingObjectFactory.class, false);
}
/**
* Returns the name factory.
*/
private synchronized DefaultNameFactory getNameFactory() {
if (nameFactory == null) {
nameFactory = (DefaultNameFactory) FactoryFinder.getNameFactory(
new Hints(Hints.NAME_FACTORY, DefaultNameFactory.class));
}
return nameFactory;
}
/**
* Returns the backing datum factory. If no factory were explicitly specified
* by the user, selects the first datum factory other than {@code this}.
*
* Note: 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.
*/
final synchronized DatumFactory getDatumFactory() throws NoSuchElementException {
if (datumFactory == null) {
DatumFactory candidate;
final Iterator it = FactoryFinder.getDatumFactories(null).iterator();
do candidate = it.next();
while (candidate == this);
datumFactory = candidate;
}
return datumFactory;
}
/**
* 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();
}
/**
* Reads 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()).isEmpty() || line.charAt(0) == '#'));
return line;
}
/**
* Reads again the {@value #ALIAS_TABLE} file into {@link #aliasMap}. This method
* may be invoked more than once in order to reload entries that have been discarded
* 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);
if (LOGGER.isLoggable(Level.CONFIG)) {
String url = aliasURL.getPath();
final int s = url.indexOf('!');
if (s >= 1) url = url.substring(0, s);
final LogRecord record = Loggings.format(Level.CONFIG, Loggings.Keys.LOADING_DATUM_ALIASES_$1, url);
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