001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2005-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.referencing.factory;
017:
018: // J2SE dependencies
019: import java.io.BufferedReader;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.InputStreamReader;
023: import java.net.URL;
024: import java.util.Date;
025: import java.util.Arrays;
026: import java.util.List;
027: import java.util.ArrayList;
028: import java.util.Map;
029: import java.util.HashMap;
030: import java.util.LinkedHashMap;
031: import java.util.Collection;
032: import java.util.Iterator;
033: import java.util.StringTokenizer;
034: import java.util.NoSuchElementException;
035: import java.util.logging.Level;
036: import java.util.logging.LogRecord;
037: import javax.units.Unit;
038:
039: // OpenGIS dependencies
040: import org.opengis.metadata.Identifier;
041: import org.opengis.referencing.AuthorityFactory; // For javadoc
042: import org.opengis.referencing.FactoryException;
043: import org.opengis.referencing.IdentifiedObject; // For javadoc
044: import org.opengis.referencing.crs.CoordinateReferenceSystem;
045: import org.opengis.referencing.datum.DatumFactory;
046: import org.opengis.referencing.datum.GeodeticDatum;
047: import org.opengis.referencing.datum.ImageDatum;
048: import org.opengis.referencing.datum.Ellipsoid;
049: import org.opengis.referencing.datum.EngineeringDatum;
050: import org.opengis.referencing.datum.TemporalDatum;
051: import org.opengis.referencing.datum.VerticalDatum;
052: import org.opengis.referencing.datum.VerticalDatumType;
053: import org.opengis.referencing.datum.PrimeMeridian;
054: import org.opengis.referencing.datum.PixelInCell;
055: import org.opengis.referencing.operation.OperationNotFoundException; // For javadoc
056: import org.opengis.util.GenericName;
057: import org.opengis.util.ScopedName;
058:
059: // Geotools dependencies
060: import org.geotools.referencing.ReferencingFactoryFinder;
061: import org.geotools.referencing.AbstractIdentifiedObject;
062: import org.geotools.referencing.datum.AbstractDatum; // For javadoc
063: import org.geotools.referencing.datum.BursaWolfParameters; // For javadoc
064: import org.geotools.util.LocalName;
065: import org.geotools.util.NameFactory;
066: import org.geotools.resources.XArray;
067: import org.geotools.resources.i18n.Logging;
068: import org.geotools.resources.i18n.LoggingKeys;
069:
070: /**
071: * A datum factory that add {@linkplain AbstractIdentifiedObject#getAlias aliases} to a datum name
072: * before to delegates the {@linkplain AbstractDatum#AbstractDatum(Map) datum creation} to an other
073: * factory. Aliases are especially important for {@linkplain AbstractDatum datum} since their
074: * {@linkplain AbstractIdentifiedObject#getName name} are often the only way to differentiate them.
075: * Two datum with different names are considered incompatible, unless some datum shift method are
076: * specified (e.g. {@linkplain BursaWolfParameters Bursa-Wolf parameters}). Unfortunatly, different
077: * softwares often use different names for the same datum,
078: * which result in {@link OperationNotFoundException} when attempting to convert coordinates from
079: * one {@linkplain CoordinateReferenceSystem coordinate reference system} to an other one. For
080: * example "<cite>Nouvelle Triangulation Française (Paris)</cite>" and
081: * "<cite>NTF (Paris meridian)</cite>" are actually the same datum. This {@code DatumAliases}
082: * class provides a way to handle that.
083: * <p>
084: * {@code DatumAliases} is a class that determines if a datum name is in our list of aliases and
085: * constructs a value for the {@linkplain IdentifiedObject#ALIAS_KEY aliases property} (as
086: * {@linkplain GenericName generic names}) for a name. The default implementation is backed by
087: * the text file "{@code DatumAliasesTable.txt}". The first line in this text file must be the
088: * authority names. All other lines are the aliases.
089: * <p>
090: * Since {@code DatumAliases} is a datum factory, any {@linkplain AuthorityFactory authority
091: * factory} or any {@linkplain org.geotools.referencing.wkt.Parser WKT parser} using this
092: * factory will takes advantage of the aliases table.
093: *
094: * @since 2.1
095: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/DatumAliases.java $
096: * @version $Id: DatumAliases.java 25050 2007-04-06 00:41:49Z jgarnett $
097: * @author Rueben Schulz
098: * @author Martin Desruisseaux
099: *
100: * @todo Invokes {@link #freeUnused} automatically after some amount of time, in order to release
101: * memory for unusued aliases. A timer should be set in {@code reload()} method.
102: *
103: * @see <A HREF="http://gdal.velocet.ca/~warmerda/wktproblems.html">WKT problems</A>
104: */
105: public class DatumAliases extends ReferencingFactory implements
106: DatumFactory {
107: /**
108: * The default file for alias table.
109: */
110: private static final String ALIAS_TABLE = "DatumAliasesTable.txt";
111:
112: /**
113: * The column separators in the file to parse.
114: */
115: private static final String SEPARATORS = ";";
116:
117: /**
118: * Array used as a marker for alias that has been discarted because never used.
119: * This array may appears in {@link #aliasMap} values.
120: *
121: * @see #freeUnused
122: */
123: private static final Object[] NEED_LOADING = new Object[0];
124:
125: /**
126: * The URL of the alias table. This file is read by {@link #reload} when first needed.
127: */
128: private final URL aliasURL;
129:
130: /**
131: * A map of our datum aliases. Keys are alias names in lower-case, and values are
132: * either {@code String[]} or {@code GenericName[]}. In order to reduce the amount
133: * of objects created, all values are initially {@code String[]} objects. They are
134: * converted to {@code GenericName[]} only when first needed.
135: */
136: private final Map/*<String,Object[]>*/aliasMap = new HashMap();
137:
138: /**
139: * The authorities. This is the first line in the alias table.
140: * This array is constructed by {@link #reload} when first needed.
141: */
142: private LocalName[] authorities;
143:
144: /**
145: * The underlying datum factory. If {@code null}, a default factory will be fetch
146: * from {@link ReferencingFactoryFinder} when first needed. A default value can't be set at
147: * construction time, since all factories may not be registered at this time.
148: */
149: private DatumFactory factory;
150:
151: /**
152: * Constructs a new datum factory with the default backing factory and alias table.
153: */
154: public DatumAliases() {
155: // Uses a slightly higher priority than the default factory, in order
156: // to get WKT parser and authorities factories to use the aliases table.
157: super (NORMAL_PRIORITY + 10);
158: aliasURL = DatumAliases.class.getResource(ALIAS_TABLE);
159: if (aliasURL == null) {
160: throw new NoSuchElementException(ALIAS_TABLE);
161: }
162: }
163:
164: /**
165: * Constructs a new datum factory using the specified factory and the default alias table.
166: *
167: * @param factory The factory to use for datum creation.
168: */
169: public DatumAliases(final DatumFactory factory) {
170: this ();
171: this .factory = factory;
172: ensureNonNull("factory", factory);
173: }
174:
175: /**
176: * Constructs a new datum factory which delegates its work to the specified factory.
177: * The aliases table is read from the specified URL. The fist line in this file most
178: * be the authority names. All other names are aliases.
179: *
180: * @param factory The factory to use for datum creation.
181: * @param aliasURL The url to the alias table.
182: */
183: public DatumAliases(final DatumFactory factory, final URL aliasURL) {
184: super (NORMAL_PRIORITY + 10);
185: this .factory = factory;
186: this .aliasURL = aliasURL;
187: ensureNonNull("factory", factory);
188: ensureNonNull("aliasURL", aliasURL);
189: }
190:
191: /**
192: * Returns the backing datum factory. If no factory were explicitly specified
193: * by the user, selects the first datum factory other than {@code this}.
194: * <p>
195: * <strong>Note:</strong> We can't invoke this method in the constructor, because the
196: * constructor is typically invoked during {@code FactoryFinder.scanForPlugins()} execution.
197: * {@code scanForPlugins} is looking for {@link DatumFactory} instances, it has not finished
198: * to search them, and invoking this method in the constructor would prematurely ask an other
199: * {@link DatumFactory} instance while the list is incomplete. Instead, we will invoke this
200: * method when the first {@code createXXX} method is invoked, which typically occurs after
201: * all factories have been initialized.
202: *
203: * @return The backing datum factory.
204: * @throws NoSuchElementException if there is no such factory.
205: */
206: private DatumFactory getDatumFactory()
207: throws NoSuchElementException {
208: assert Thread.holdsLock(this );
209: if (factory == null) {
210: DatumFactory candidate;
211: final Iterator it = ReferencingFactoryFinder
212: .getDatumFactories(null).iterator();
213: do
214: candidate = (DatumFactory) it.next();
215: while (candidate == this );
216: factory = candidate;
217: }
218: return factory;
219: }
220:
221: /**
222: * Returns a caseless version of the specified key, to be stored in the map.
223: */
224: private static String toCaseless(final String key) {
225: return key.replace('_', ' ').trim().toLowerCase();
226: }
227:
228: /**
229: * Read the next line from the specified input stream, skipping all blank
230: * and comment lines. Returns {@code null} on end of stream.
231: */
232: private static String readLine(final BufferedReader in)
233: throws IOException {
234: String line;
235: do
236: line = in.readLine();
237: while (line != null
238: && ((line = line.trim()).length() == 0 || line
239: .charAt(0) == '#'));
240: return line;
241: }
242:
243: /**
244: * Read again the "{@code DatumAliasesTable.txt}" file into {@link #aliasMap}.
245: * This method may be invoked more than once in order to reload entries that
246: * have been discarted by {@link #freeUnused}. This method assumes that the
247: * file content didn't change between two calls.
248: *
249: * @throws IOException if the loading failed.
250: */
251: private void reload() throws IOException {
252: assert Thread.holdsLock(this );
253: LOGGER.log(Logging.format(Level.FINE,
254: LoggingKeys.LOADING_DATUM_ALIASES_$1, aliasURL));
255: final BufferedReader in = new BufferedReader(
256: new InputStreamReader(aliasURL.openStream()));
257: /*
258: * Parses the title line. This line contains authority names as column titles.
259: * The authority names will be used as the scope for each identifiers to be
260: * created.
261: */
262: String line = readLine(in);
263: if (line != null) {
264: final List elements/*<Object>*/= new ArrayList();
265: StringTokenizer st = new StringTokenizer(line, SEPARATORS);
266: while (st.hasMoreTokens()) {
267: final String name = st.nextToken().trim();
268: elements.add(name.length() != 0 ? new LocalName(name)
269: : null);
270: }
271: authorities = (LocalName[]) elements
272: .toArray(new LocalName[elements.size()]);
273: final Map/*<String,String>*/canonical = new HashMap();
274: /*
275: * Parses all aliases. They are stored as arrays of strings for now, but will be
276: * converted to array of generic names by {@link #getAliases} when first needed.
277: * If the alias belong to an authority (which should be true in most cases), a
278: * scoped name will be created at this time.
279: */
280: while ((line = readLine(in)) != null) {
281: elements.clear();
282: canonical.clear();
283: st = new StringTokenizer(line, SEPARATORS);
284: while (st.hasMoreTokens()) {
285: String alias = st.nextToken().trim();
286: if (alias.length() != 0) {
287: final String previous = (String) canonical.put(
288: alias, alias);
289: if (previous != null) {
290: canonical.put(previous, previous);
291: alias = previous;
292: }
293: } else {
294: alias = null;
295: }
296: elements.add(alias);
297: }
298: // Trim trailing null values only (we must keep other null values).
299: for (int i = elements.size(); --i >= 0;) {
300: if (elements.get(i) != null)
301: break;
302: elements.remove(i);
303: }
304: if (!elements.isEmpty()) {
305: /*
306: * Copies the aliases array in the aliases map for all local names. If a
307: * previous value is found as an array of GenericName objects, those generic
308: * names are conserved in the map (instead of the string values parsed above)
309: * in order to avoid constructing them again when they will be needed.
310: */
311: final String[] names = (String[]) elements
312: .toArray(new String[elements.size()]);
313: for (int i = 0; i < names.length; i++) {
314: final String name = names[i];
315: final String key = toCaseless(name);
316: final Object[] previous = (Object[]) aliasMap
317: .put(key, names);
318: if (previous != null
319: && previous != NEED_LOADING) {
320: if (previous instanceof GenericName[]) {
321: aliasMap.put(key, previous);
322: } else if (!Arrays.equals(previous, names)) {
323: // TODO: localize
324: LOGGER
325: .warning("Inconsistent aliases for datum \""
326: + name + "\".");
327: }
328: }
329: }
330: }
331: }
332: }
333: in.close();
334: }
335:
336: /**
337: * Logs an {@link IOException}.
338: */
339: private void log(final IOException exception) {
340: LogRecord record = Logging.format(Level.WARNING,
341: LoggingKeys.CANT_READ_FILE_$1, aliasURL);
342: record.setSourceClassName(DatumAliases.class.getName());
343: record.setSourceMethodName("reload");
344: record.setThrown(exception);
345: LOGGER.log(record);
346: }
347:
348: /**
349: * Returns the aliases, as a set of {@link GenericName}, for the given name.
350: * This method returns an internal array; do not modify the returned value.
351: *
352: * @param name Datum alias name to lookup.
353: * @return A set of datum aliases as {@link GenericName} objects for the given name,
354: * or {@code null} if the name is not in our list of aliases.
355: *
356: * @see #addAliases
357: * @see #reload
358: */
359: private GenericName[] getAliases(String name) {
360: assert Thread.holdsLock(this );
361: if (aliasMap.isEmpty())
362: try {
363: reload();
364: } catch (IOException exception) {
365: log(exception);
366: // Continue in case the requested alias has been read before the failure occured.
367: }
368: /*
369: * Gets the aliases for the specified name. If an entry exists for this name with a null
370: * value, this means that 'freeUnused()' has been invoked previously. Reload the file and
371: * try again since the requested name may be one of the set of discarted aliases.
372: */
373: name = toCaseless(name);
374: Object[] aliases = (Object[]) aliasMap.get(name);
375: if (aliases == null) {
376: // Unknow name. We are done.
377: return null;
378: }
379: if (aliases == NEED_LOADING) {
380: // Known name, but the list of alias has been previously
381: // discarted because never used. Reload the file.
382: try {
383: reload();
384: } catch (IOException exception) {
385: log(exception);
386: // Continue in case the requested alias has been read before the failure occured.
387: }
388: aliases = (Object[]) aliasMap.get(name);
389: if (aliases == NEED_LOADING) {
390: // Should never happen, unless reloading failed or some lines have
391: // been deleted in the file since last time the file has been loaded.
392: return null;
393: }
394: }
395: if (aliases instanceof GenericName[]) {
396: return (GenericName[]) aliases;
397: }
398: /*
399: * Aliases has been found, but available as an array of strings only. This means
400: * that those aliases have never been requested before. Transforms the array of
401: * strings into an array of generic names. The new array replaces the old one for
402: * all aliases enumerated in the array (not just the requested one).
403: */
404: int count = 0;
405: GenericName[] names = new GenericName[aliases.length];
406: for (int i = 0; i < aliases.length; i++) {
407: final CharSequence alias = (CharSequence) aliases[i];
408: if (alias != null) {
409: if (count < authorities.length) {
410: final LocalName authority = authorities[count];
411: if (authority != null) {
412: names[count++] = new org.geotools.util.ScopedName(
413: authority, alias);
414: continue;
415: }
416: }
417: names[count++] = new LocalName(alias);
418: }
419: }
420: names = (GenericName[]) XArray.resize(names, count);
421: for (int i = 0; i < names.length; i++) {
422: final String alias = names[i].asLocalName().toString();
423: final Object[] previous = (Object[]) aliasMap.put(
424: toCaseless(alias), names);
425: assert previous == names
426: || Arrays.equals(aliases, previous) : alias;
427: }
428: return names;
429: }
430:
431: /**
432: * Completes the given map of properties. This method expects a map of properties to
433: * be given to {@link AbstractDatum#AbstractDatum(Map)} constructor. The name is fetch
434: * from the {@link IdentifiedObject#NAME_KEY NAME_KEY}.
435: * The {@link AbstractIdentifiedObject#ALIAS_KEY ALIAS_KEY} is
436: * completed with the aliases know to this factory.
437: *
438: * @param properties The set of properties to complete.
439: * @return The completed properties, or {@code properties} if no change were done.
440: *
441: * @see #getAliases
442: */
443: private Map addAliases(Map properties) {
444: ensureNonNull("properties", properties);
445: Object value = properties.get(IdentifiedObject.NAME_KEY);
446: ensureNonNull("name", value);
447: final String name;
448: if (value instanceof Identifier) {
449: name = ((Identifier) value).getCode();
450: } else {
451: name = value.toString();
452: }
453: GenericName[] aliases = getAliases(name);
454: if (aliases != null) {
455: /*
456: * Aliases have been found. Before to add them to the properties map, overrides them
457: * with the aliases already provided by the users, if any. The 'merged' map is the
458: * union of aliases know to this factory and aliases provided by the user. User's
459: * aliases will be added first, for preserving the user's order (the LinkedHashMap
460: * acts as a FIFO queue).
461: */
462: int count = aliases.length;
463: value = properties.get(IdentifiedObject.ALIAS_KEY);
464: if (value != null) {
465: final Map merged/*<String,GenericName>*/= new LinkedHashMap();
466: putAll(NameFactory.toArray(value), merged);
467: count -= putAll(aliases, merged);
468: final Collection c = merged.values();
469: aliases = (GenericName[]) c.toArray(new GenericName[c
470: .size()]);
471: }
472: /*
473: * Now set the aliases. This replacement will not be performed if
474: * all our aliases were replaced by user's aliases (count <= 0).
475: */
476: if (count > 0) {
477: properties = new HashMap(properties);
478: properties.put(IdentifiedObject.ALIAS_KEY, aliases);
479: }
480: }
481: return properties;
482: }
483:
484: /**
485: * Put all elements in the {@code names} array into the specified map. Order matter, since the
486: * first element in the array should be the first element returned by the map if the map is
487: * actually an instance of {@link LinkedHashMap}. This method returns the number of elements
488: * ignored.
489: */
490: private static final int putAll(final GenericName[] names,
491: final Map map) {
492: int ignored = 0;
493: for (int i = 0; i < names.length; i++) {
494: final GenericName name = names[i];
495: final GenericName scoped = name.asScopedName();
496: final String key = toCaseless((scoped != null ? scoped
497: : name).toString());
498: final GenericName old = (GenericName) map.put(key, name);
499: if (old instanceof ScopedName) {
500: map.put(key, old); // Preserves the user value, except if it was unscoped.
501: ignored++;
502: }
503: }
504: return ignored;
505: }
506:
507: /**
508: * Creates an engineering datum.
509: *
510: * @param properties Name and other properties to give to the new object.
511: * @throws FactoryException if the object creation failed.
512: */
513: public synchronized EngineeringDatum createEngineeringDatum(
514: final Map properties) throws FactoryException {
515: return getDatumFactory().createEngineeringDatum(
516: addAliases(properties));
517: }
518:
519: /**
520: * Creates geodetic datum from ellipsoid and (optionaly) Bursa-Wolf parameters.
521: *
522: * @param properties Name and other properties to give to the new object.
523: * @param ellipsoid Ellipsoid to use in new geodetic datum.
524: * @param primeMeridian Prime meridian to use in new geodetic datum.
525: * @throws FactoryException if the object creation failed.
526: */
527: public synchronized GeodeticDatum createGeodeticDatum(
528: final Map properties, final Ellipsoid ellipsoid,
529: final PrimeMeridian primeMeridian) throws FactoryException {
530: return getDatumFactory().createGeodeticDatum(
531: addAliases(properties), ellipsoid, primeMeridian);
532: }
533:
534: /**
535: * Creates an image datum.
536: *
537: * @param properties Name and other properties to give to the new object.
538: * @param pixelInCell Specification of the way the image grid is associated
539: * with the image data attributes.
540: * @throws FactoryException if the object creation failed.
541: */
542: public synchronized ImageDatum createImageDatum(
543: final Map properties, final PixelInCell pixelInCell)
544: throws FactoryException {
545: return getDatumFactory().createImageDatum(
546: addAliases(properties), pixelInCell);
547: }
548:
549: /**
550: * Creates a temporal datum from an enumerated type value.
551: *
552: * @param properties Name and other properties to give to the new object.
553: * @param origin The date and time origin of this temporal datum.
554: * @throws FactoryException if the object creation failed.
555: */
556: public synchronized TemporalDatum createTemporalDatum(
557: final Map properties, final Date origin)
558: throws FactoryException {
559: return getDatumFactory().createTemporalDatum(
560: addAliases(properties), origin);
561: }
562:
563: /**
564: * Creates a vertical datum from an enumerated type value.
565: *
566: * @param properties Name and other properties to give to the new object.
567: * @param type The type of this vertical datum (often “geoidal”).
568: * @throws FactoryException if the object creation failed.
569: */
570: public synchronized VerticalDatum createVerticalDatum(
571: final Map properties, final VerticalDatumType type)
572: throws FactoryException {
573: return getDatumFactory().createVerticalDatum(
574: addAliases(properties), type);
575: }
576:
577: /**
578: * Creates an ellipsoid from radius values.
579: *
580: * @param properties Name and other properties to give to the new object.
581: * @param semiMajorAxis Equatorial radius in supplied linear units.
582: * @param semiMinorAxis Polar radius in supplied linear units.
583: * @param unit Linear units of ellipsoid axes.
584: * @throws FactoryException if the object creation failed.
585: */
586: public synchronized Ellipsoid createEllipsoid(final Map properties,
587: final double semiMajorAxis, final double semiMinorAxis,
588: final Unit unit) throws FactoryException {
589: return getDatumFactory().createEllipsoid(
590: addAliases(properties), semiMajorAxis, semiMinorAxis,
591: unit);
592: }
593:
594: /**
595: * Creates an ellipsoid from an major radius, and inverse flattening.
596: *
597: * @param properties Name and other properties to give to the new object.
598: * @param semiMajorAxis Equatorial radius in supplied linear units.
599: * @param inverseFlattening Eccentricity of ellipsoid.
600: * @param unit Linear units of major axis.
601: * @throws FactoryException if the object creation failed.
602: */
603: public synchronized Ellipsoid createFlattenedSphere(
604: final Map properties, final double semiMajorAxis,
605: final double inverseFlattening, final Unit unit)
606: throws FactoryException {
607: return getDatumFactory().createFlattenedSphere(
608: addAliases(properties), semiMajorAxis,
609: inverseFlattening, unit);
610: }
611:
612: /**
613: * Creates a prime meridian, relative to Greenwich.
614: *
615: * @param properties Name and other properties to give to the new object.
616: * @param longitude Longitude of prime meridian in supplied angular units East of Greenwich.
617: * @param angularUnit Angular units of longitude.
618: * @throws FactoryException if the object creation failed.
619: */
620: public synchronized PrimeMeridian createPrimeMeridian(
621: final Map properties, final double longitude,
622: final Unit angularUnit) throws FactoryException {
623: return getDatumFactory().createPrimeMeridian(
624: addAliases(properties), longitude, angularUnit);
625: }
626:
627: /**
628: * Free all aliases that have been unused up to date. If one of those alias is needed at a
629: * later time, the aliases table will be reloaded.
630: */
631: public synchronized void freeUnused() {
632: if (aliasMap != null) {
633: for (final Iterator it = aliasMap.entrySet().iterator(); it
634: .hasNext();) {
635: final Map.Entry entry = (Map.Entry) it.next();
636: if (!(entry.getValue() instanceof GenericName[])) {
637: entry.setValue(NEED_LOADING);
638: }
639: }
640: }
641: }
642: }
|