/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.wkt; import java.io.BufferedReader; import java.text.ParseException; import java.text.ParsePosition; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.measure.unit.NonSI; import javax.measure.unit.SI; import javax.measure.unit.Unit; import javax.measure.quantity.Angle; import javax.measure.quantity.Length; import javax.measure.quantity.Quantity; import static java.util.Collections.singletonMap; import org.opengis.metadata.citation.Citation; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchIdentifierException; // While start import is usually a deprecated practice, we use such a large amount // of interfaces in those packages that it we choose to exceptionnaly use * here. import org.opengis.referencing.cs.*; import org.opengis.referencing.crs.*; import org.opengis.referencing.datum.*; import org.opengis.referencing.operation.*; import org.geotools.factory.Hints; import org.geotools.metadata.iso.citation.Citations; import org.geotools.referencing.ReferencingFactoryFinder; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.datum.BursaWolfParameters; import org.geotools.referencing.datum.DefaultGeodeticDatum; import org.geotools.referencing.datum.DefaultPrimeMeridian; import org.geotools.referencing.datum.DefaultVerticalDatum; import org.geotools.referencing.cs.AbstractCS; import org.geotools.referencing.cs.DefaultCoordinateSystemAxis; import org.geotools.referencing.factory.ReferencingFactoryContainer; import org.geotools.referencing.operation.DefiningConversion; import org.geotools.resources.Arguments; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Parser for * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well * Known Text</cite> (WKT)</A>. This parser can parse {@linkplain MathTransform math transform} * objects as well, which is part of the WKT's {@code FITTED_CS} element. * * @since 2.0 * @source $URL$ * @version $Id$ * @author Remi Eve * @author Martin Desruisseaux (IRD) * * @see <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well Know Text specification</A> * @see <A HREF="http://gdal.velocet.ca/~warmerda/wktproblems.html">OGC WKT Coordinate System Issues</A> */ public class Parser extends MathTransformParser { /** * For cross-version compatibility. */ private static final long serialVersionUID = -144097689843465085L; /** * {@code true} in order to allows the non-standard Oracle syntax. Oracle put the Bursa-Wolf * parameters straight into the {@code DATUM} elements, without enclosing them in a * {@code TOWGS84} element. */ private static final boolean ALLOW_ORACLE_SYNTAX = true; /** * The mapping between WKT element name and the object class to be created. * Will be created by {@link #getTypeMap} only when first needed. Keys must * be upper case. */ private static Map<String,Class<?>> TYPES; /** * The factory to use for creating {@linkplain Datum datum}. */ protected final DatumFactory datumFactory; /** * The factory to use for creating {@linkplain CoordinateSystem coordinate systems}. */ protected final CSFactory csFactory; /** * The factory to use for creating {@linkplain CoordinateReferenceSystem * coordinate reference systems}. */ protected final CRSFactory crsFactory; /** * The list of {@linkplain AxisDirection axis directions} from their name. */ private final Map<String,AxisDirection> directions; /** * Constructs a parser using the default set of symbols and factories. */ public Parser() { this(Symbols.DEFAULT); } /** * Constructs a parser for the specified set of symbols using default factories. * * @param symbols The symbols for parsing and formatting numbers. * * @todo Pass hints in argument. */ public Parser(final Symbols symbols) { this(symbols, ReferencingFactoryFinder.getDatumFactory (null), ReferencingFactoryFinder.getCSFactory (null), ReferencingFactoryFinder.getCRSFactory (null), ReferencingFactoryFinder.getMathTransformFactory(null)); } /** * Constructs a parser for the specified set of symbols using the specified set of factories. * * @param symbols The symbols for parsing and formatting numbers. * @param factories The factories to use. */ public Parser(final Symbols symbols, final ReferencingFactoryContainer factories) { this(symbols, factories.getDatumFactory(), factories.getCSFactory(), factories.getCRSFactory(), factories.getMathTransformFactory()); } /** * Constructs a parser for the specified set of symbols using the specified factories. * * @param symbols The symbols for parsing and formatting numbers. * @param datumFactory The factory to use for creating {@linkplain Datum datum}. * @param csFactory The factory to use for creating {@linkplain CoordinateSystem * coordinate systems}. * @param crsFactory The factory to use for creating {@linkplain CoordinateReferenceSystem * coordinate reference systems}. * @param mtFactory The factory to use for creating {@linkplain MathTransform * math transform} objects. */ public Parser(final Symbols symbols, final DatumFactory datumFactory, final CSFactory csFactory, final CRSFactory crsFactory, final MathTransformFactory mtFactory) { super(symbols, mtFactory); this.datumFactory = datumFactory; this. csFactory = csFactory; this.crsFactory = crsFactory; final AxisDirection[] values = AxisDirection.values(); directions = new HashMap<String,AxisDirection>( (int) Math.ceil((values.length + 1) / 0.75f), 0.75f); for (int i=0; i<values.length; i++) { directions.put(values[i].name().trim().toUpperCase(), values[i]); } } /** * Parses a coordinate reference system element. * * @param text The text to be parsed. * @return The coordinate reference system. * @throws ParseException if the string can't be parsed. */ public CoordinateReferenceSystem parseCoordinateReferenceSystem(final String text) throws ParseException { final Element element = getTree(text, new ParsePosition(0)); final CoordinateReferenceSystem crs = parseCoordinateReferenceSystem(element); element.close(); return crs; } /** * Parses a coordinate reference system element. * * @param parent The parent element. * @return The next element as a {@link CoordinateReferenceSystem} object. * @throws ParseException if the next element can't be parsed. */ private CoordinateReferenceSystem parseCoordinateReferenceSystem(final Element element) throws ParseException { final Object key = element.peek(); if (key instanceof Element) { final String keyword = ((Element) key).keyword.trim().toUpperCase(symbols.locale); CoordinateReferenceSystem r = null; try { if ( "GEOGCS".equals(keyword)) return r=parseGeoGCS (element); if ( "PROJCS".equals(keyword)) return r=parseProjCS (element); if ( "GEOCCS".equals(keyword)) return r=parseGeoCCS (element); if ( "VERT_CS".equals(keyword)) return r=parseVertCS (element); if ( "LOCAL_CS".equals(keyword)) return r=parseLocalCS (element); if ( "COMPD_CS".equals(keyword)) return r=parseCompdCS (element); if ("FITTED_CS".equals(keyword)) return r=parseFittedCS(element); } finally { // Work around for simulating post-conditions in Java. assert isValid(r, keyword) : element; } } throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, key)); } /** * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree. * * @param element The element to be parsed. * @return The object. * @throws ParseException if the element can't be parsed. * * @todo All sequences of <code>if ("FOO".equals(keyword))</code> in this method * and other methods of this class and subclasses, could be optimized with * a {@code switch} statement. */ @Override protected Object parse(final Element element) throws ParseException { final Object key = element.peek(); if (key instanceof Element) { final String keyword = ((Element) key).keyword.trim().toUpperCase(symbols.locale); Object r = null; try { if ( "AXIS".equals(keyword)) return r=parseAxis (element, SI.METER, true); if ( "PRIMEM".equals(keyword)) return r=parsePrimem (element, NonSI.DEGREE_ANGLE); if ( "TOWGS84".equals(keyword)) return r=parseToWGS84 (element); if ( "SPHEROID".equals(keyword)) return r=parseSpheroid (element); if ( "VERT_DATUM".equals(keyword)) return r=parseVertDatum (element); if ("LOCAL_DATUM".equals(keyword)) return r=parseLocalDatum(element); if ( "DATUM".equals(keyword)) return r=parseDatum (element, DefaultPrimeMeridian.GREENWICH); r = parseMathTransform(element, false); if (r != null) { return r; } } finally { // Work around for simulating post-conditions in Java. assert isValid(r, keyword) : element; } } return parseCoordinateReferenceSystem(element); } /** * Checks if the parsed object is of the expected type. This is also a way to check * the consistency of the {@link #TYPES} map. */ private static boolean isValid(final Object parsed, final String keyword) { if (parsed == null) { // Required in order to avoid AssertionError in place of ParseException. return true; } final Class type = getClassOf(keyword); return type!=null && type.isInstance(parsed); } /** * Returns the properties to be given to the parsed object. This method is invoked * automatically by the parser for the {@linkplain Element#isRoot root element} only. * This method expect on input the properties parsed from the {@code AUTHORITY} element, * and returns on output the properties to give to the object to be created. The default * implementation returns the {@code properties} map unchanged. Subclasses may override * this method in order to add or change properties. * <p> * <strong>Example:</strong> if a subclass want to add automatically an authority code when * no {@code AUTHORITY} element was explicitly set in the WKT, then it may test for the * {@link IdentifiedObject#IDENTIFIERS_KEY} key and add automatically an entry if this * key was missing. * * @param properties The properties parsed from the WKT file. Entries can be added, removed * or modified directly in this map. * @return The properties to be given to the parsed object. This is usually {@code properties} * (maybe after modifications), but could also be a new map. * * @since 2.3 */ protected Map<String,Object> alterProperties(final Map<String,Object> properties) { return properties; } /** * Parses an <strong>optional</strong> "AUTHORITY" element. * This element has the following pattern: * * <blockquote><code> * AUTHORITY["<name>", "<code>"] * </code></blockquote> * or even * <blockquote><code> * AUTHORITY["<name>", <code>] * </code></blockquote> * * @param parent The parent element. * @param name The name of the parent object being parsed. * @return A properties map with the parent name and the optional autority code. * @throws ParseException if the "AUTHORITY" can't be parsed. */ private Map<String,Object> parseAuthority(final Element parent, final String name) throws ParseException { final boolean isRoot = parent.isRoot(); final Element element = parent.pullOptionalElement("AUTHORITY"); Map<String,Object> properties; if (element == null) { if (isRoot) { properties = new HashMap<String,Object>(4); properties.put(IdentifiedObject.NAME_KEY, name); } else { properties = singletonMap(IdentifiedObject.NAME_KEY, (Object) name); } } else { final String auth = element.pullString("name"); // the code can be annotation marked but could be a number to String code = element.pullOptionalString("code"); if (code == null) { int codeNumber = element.pullInteger("code"); code = String.valueOf(codeNumber); } element.close(); final Citation authority = Citations.fromName(auth); properties = new HashMap<String,Object>(4); properties.put(IdentifiedObject. NAME_KEY, new NamedIdentifier(authority, name)); properties.put(IdentifiedObject.IDENTIFIERS_KEY, new NamedIdentifier(authority, code)); } if (isRoot) { properties = alterProperties(properties); } return properties; } /** * Parses an "UNIT" element. * This element has the following pattern: * * <blockquote><code> * UNIT["<name>", <conversion factor> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @param unit The contextual unit. Usually {@link SI#METRE} or {@link SI#RADIAN}. * @return The "UNIT" element as an {@link Unit} object. * @throws ParseException if the "UNIT" can't be parsed. * * @todo Authority code is currently ignored. We may consider to create a subclass of * {@link Unit} which implements {@link IdentifiedObject} in a future version. */ private <T extends Quantity> Unit<T> parseUnit(final Element parent, final Unit<T> unit) throws ParseException { final Element element = parent.pullElement("UNIT"); final String name = element.pullString("name"); final double factor = element.pullDouble("factor"); final Map<String,?> properties = parseAuthority(element, name); element.close(); return (factor != 1) ? unit.times(factor) : unit; } /** * Parses an "AXIS" element. * This element has the following pattern: * * <blockquote><code> * AXIS["<name>", NORTH | SOUTH | EAST | WEST | UP | DOWN | OTHER] * </code></blockquote> * * Note: there is no AUTHORITY element for AXIS element in OGC specification. However, we * accept it anyway in order to make the parser more tolerant to non-100% compliant * WKT. Note that AXIS is really the only element without such AUTHORITY clause and * the EPSG database provides authority code for all axis. * * @param parent The parent element. * @param unit The contextual unit. Usually {@link NonSI#DEGREE_ANGLE} or {@link SI#METRE}. * @param required {@code true} if the axis is mandatory, * or {@code false} if it is optional. * @return The "AXIS" element as a {@link CoordinateSystemAxis} object, or {@code null} * if the axis was not required and there is no axis object. * @throws ParseException if the "AXIS" element can't be parsed. */ private CoordinateSystemAxis parseAxis(final Element parent, final Unit<?> unit, final boolean required) throws ParseException { final Element element; if (required) { element = parent.pullElement("AXIS"); } else { element = parent.pullOptionalElement("AXIS"); if (element == null) { return null; } } final String name = element.pullString ("name"); final Element orientation = element.pullVoidElement("orientation"); final Map<String,?> properties = parseAuthority(element, name); // See javadoc element.close(); final AxisDirection direction = directions.get(orientation.keyword.trim().toUpperCase()); if (direction == null) { throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, orientation)); } try { return createAxis(properties, name, direction, unit); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Creates an axis. If the name matches one of pre-defined axis, the pre-defined one * will be returned. This replacement help to get more success when comparing a CS * built from WKT against a CS built from one of Geotools's constants. * * @param properties Name and other properties to give to the new object. * If {@code null}, the abbreviation will be used as the axis name. * @param abbreviation The coordinate axis abbreviation. * @param direction The axis direction. * @param unit The coordinate axis unit. * @throws FactoryException if the axis can't be created. */ private CoordinateSystemAxis createAxis(Map<String,?> properties, final String abbreviation, final AxisDirection direction, final Unit<?> unit) throws FactoryException { final CoordinateSystemAxis candidate = DefaultCoordinateSystemAxis.getPredefined(abbreviation, direction); if (candidate != null && unit.equals(candidate.getUnit())) { return candidate; } if (properties == null) { properties = singletonMap(IdentifiedObject.NAME_KEY, abbreviation); } return csFactory.createCoordinateSystemAxis(properties, abbreviation, direction, unit); } /** * Parses a "PRIMEM" element. This element has the following pattern: * * <blockquote><code> * PRIMEM["<name>", <longitude> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @param angularUnit The contextual unit. * @return The "PRIMEM" element as a {@link PrimeMeridian} object. * @throws ParseException if the "PRIMEM" element can't be parsed. */ private PrimeMeridian parsePrimem(final Element parent, final Unit<Angle> angularUnit) throws ParseException { final Element element = parent.pullElement("PRIMEM"); final String name = element.pullString("name"); final double longitude = element.pullDouble("longitude"); final Map<String,?> properties = parseAuthority(element, name); element.close(); try { return datumFactory.createPrimeMeridian(properties, longitude, angularUnit); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses an <strong>optional</strong> "TOWGS84" element. * This element has the following pattern: * * <blockquote><code> * TOWGS84[<dx>, <dy>, <dz>, <ex>, <ey>, <ez>, <ppm>] * </code></blockquote> * * @param parent The parent element. * @return The "TOWGS84" element as a {@link BursaWolfParameters} object, * or {@code null} if no "TOWGS84" has been found. * @throws ParseException if the "TOWGS84" can't be parsed. */ private static BursaWolfParameters parseToWGS84(final Element parent) throws ParseException { final Element element = parent.pullOptionalElement("TOWGS84"); if (element == null) { return null; } final BursaWolfParameters info = new BursaWolfParameters(DefaultGeodeticDatum.WGS84); info.dx = element.pullDouble("dx"); info.dy = element.pullDouble("dy"); info.dz = element.pullDouble("dz"); if (element.peek() != null) { info.ex = element.pullDouble("ex"); info.ey = element.pullDouble("ey"); info.ez = element.pullDouble("ez"); info.ppm = element.pullDouble("ppm"); } element.close(); return info; } /** * Parses a "SPHEROID" element. This element has the following pattern: * * <blockquote><code> * SPHEROID["<name>", <semi-major axis>, <inverse flattening> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "SPHEROID" element as an {@link Ellipsoid} object. * @throws ParseException if the "SPHEROID" element can't be parsed. */ private Ellipsoid parseSpheroid(final Element parent) throws ParseException { Element element = parent.pullElement("SPHEROID"); String name = element.pullString("name"); double semiMajorAxis = element.pullDouble("semiMajorAxis"); double inverseFlattening = element.pullDouble("inverseFlattening"); Map<String,?> properties = parseAuthority(element, name); element.close(); if (inverseFlattening == 0) { // Inverse flattening null is an OGC convention for a sphere. inverseFlattening = Double.POSITIVE_INFINITY; } try { return datumFactory.createFlattenedSphere(properties, semiMajorAxis, inverseFlattening, SI.METER); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "PROJECTION" element. This element has the following pattern: * * <blockquote><code> * PROJECTION["<name>" {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @param ellipsoid The ellipsoid, or {@code null} if none. * @param linearUnit The linear unit of the parent PROJCS element, or {@code null}. * @param angularUnit The angular unit of the parent GEOCS element, or {@code null}. * @return The "PROJECTION" element as a {@link ParameterValueGroup} object. * @throws ParseException if the "PROJECTION" element can't be parsed. */ private ParameterValueGroup parseProjection(final Element parent, final Ellipsoid ellipsoid, final Unit<Length> linearUnit, final Unit<Angle> angularUnit) throws ParseException { final Element element = parent.pullElement("PROJECTION"); final String classification = element.pullString("name"); final Map<String,?> properties = parseAuthority(element, classification); element.close(); /* * Set the list of parameters. NOTE: Parameters are defined in * the parent Element (usually a "PROJCS" element), not in this * "PROJECTION" element. * * We will set the semi-major and semi-minor parameters from the * ellipsoid first. If those values were explicitly specified in * a "PARAMETER" statement, they will overwrite the values inferred * from the ellipsoid. */ final ParameterValueGroup parameters; try { parameters = mtFactory.getDefaultParameters(classification); } catch (NoSuchIdentifierException exception) { throw element.parseFailed(exception, null); } Element param = parent; try { if (ellipsoid != null) { final Unit<Length> axisUnit = ellipsoid.getAxisUnit(); parameters.parameter("semi_major").setValue(ellipsoid.getSemiMajorAxis(), axisUnit); parameters.parameter("semi_minor").setValue(ellipsoid.getSemiMinorAxis(), axisUnit); } while ((param=parent.pullOptionalElement("PARAMETER")) != null) { final String paramName = param.pullString("name"); final double paramValue = param.pullDouble("value"); final ParameterValue<?> parameter = parameters.parameter(paramName); final Unit<?> expected = parameter.getDescriptor().getUnit(); if (expected!=null && !Unit.ONE.equals(expected)) { if (linearUnit!=null && SI.METER.isCompatible(expected)) { parameter.setValue(paramValue, linearUnit); continue; } if (angularUnit!=null && SI.RADIAN.isCompatible(expected)) { parameter.setValue(paramValue, angularUnit); continue; } } parameter.setValue(paramValue); } } catch (ParameterNotFoundException exception) { throw param.parseFailed(exception, Errors.format(ErrorKeys.UNEXPECTED_PARAMETER_$1, exception.getParameterName())); } return parameters; } /** * Parses a "DATUM" element. This element has the following pattern: * * <blockquote><code> * DATUM["<name>", <spheroid> {,<to wgs84>} {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @param meridian the prime meridian. * @return The "DATUM" element as a {@link GeodeticDatum} object. * @throws ParseException if the "DATUM" element can't be parsed. */ private GeodeticDatum parseDatum(final Element parent, final PrimeMeridian meridian) throws ParseException { Element element = parent.pullElement("DATUM"); String name = element.pullString("name"); Ellipsoid ellipsoid = parseSpheroid(element); BursaWolfParameters toWGS84 = parseToWGS84(element); // Optional; may be null. Map<String,Object> properties = parseAuthority(element, name); if (ALLOW_ORACLE_SYNTAX && (toWGS84 == null) && (element.peek() instanceof Number)) { toWGS84 = new BursaWolfParameters(DefaultGeodeticDatum.WGS84); toWGS84.dx = element.pullDouble("dx"); toWGS84.dy = element.pullDouble("dy"); toWGS84.dz = element.pullDouble("dz"); toWGS84.ex = element.pullDouble("ex"); toWGS84.ey = element.pullDouble("ey"); toWGS84.ez = element.pullDouble("ez"); toWGS84.ppm = element.pullDouble("ppm"); } element.close(); if (toWGS84 != null) { if (!(properties instanceof HashMap)) { properties = new HashMap<String,Object>(properties); } properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, toWGS84); } try { return datumFactory.createGeodeticDatum(properties, ellipsoid, meridian); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "VERT_DATUM" element. This element has the following pattern: * * <blockquote><code> * VERT_DATUM["<name>", <datum type> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "VERT_DATUM" element as a {@link VerticalDatum} object. * @throws ParseException if the "VERT_DATUM" element can't be parsed. */ private VerticalDatum parseVertDatum(final Element parent) throws ParseException { final Element element = parent.pullElement("VERT_DATUM"); final String name = element.pullString ("name"); final int datum = element.pullInteger("datum"); final Map<String,?> properties = parseAuthority(element, name); element.close(); final VerticalDatumType type = DefaultVerticalDatum.getVerticalDatumTypeFromLegacyCode(datum); if (type == null) { throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, datum)); } try { return datumFactory.createVerticalDatum(properties, type); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "LOCAL_DATUM" element. This element has the following pattern: * * <blockquote><code> * LOCAL_DATUM["<name>", <datum type> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "LOCAL_DATUM" element as an {@link EngineeringDatum} object. * @throws ParseException if the "LOCAL_DATUM" element can't be parsed. * * @todo The vertical datum type is currently ignored. */ private EngineeringDatum parseLocalDatum(final Element parent) throws ParseException { final Element element = parent.pullElement("LOCAL_DATUM"); final String name = element.pullString ("name"); final int datum = element.pullInteger("datum"); final Map<String,?> properties = parseAuthority(element, name); element.close(); try { return datumFactory.createEngineeringDatum(properties); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "LOCAL_CS" element. * This element has the following pattern: * * <blockquote><code> * LOCAL_CS["<name>", <local datum>, <unit>, <axis>, {,<axis>}* {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "LOCAL_CS" element as an {@link EngineeringCRS} object. * @throws ParseException if the "LOCAL_CS" element can't be parsed. * * @todo The coordinate system used is always a Geotools implementation, since we don't * know which method to invokes in the {@link CSFactory} (is it a cartesian * coordinate system? a spherical one? etc.). */ private EngineeringCRS parseLocalCS(final Element parent) throws ParseException { Element element = parent.pullElement("LOCAL_CS"); String name = element.pullString("name"); EngineeringDatum datum = parseLocalDatum(element); Unit<Length> linearUnit = parseUnit(element, SI.METER); CoordinateSystemAxis axis = parseAxis(element, linearUnit, true); List<CoordinateSystemAxis> list = new ArrayList<CoordinateSystemAxis>(); do { list.add(axis); axis = parseAxis(element, linearUnit, false); } while (axis != null); final Map<String,?> properties = parseAuthority(element, name); element.close(); final CoordinateSystem cs; cs = new AbstractCS(singletonMap("name", name), list.toArray(new CoordinateSystemAxis[list.size()])); try { return crsFactory.createEngineeringCRS(properties, datum, cs); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "GEOCCS" element. * This element has the following pattern: * * <blockquote><code> * GEOCCS["<name>", <datum>, <prime meridian>, <linear unit> * {,<axis> ,<axis> ,<axis>} {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "GEOCCS" element as a {@link GeocentricCRS} object. * @throws ParseException if the "GEOCCS" element can't be parsed. */ private GeocentricCRS parseGeoCCS(final Element parent) throws ParseException { final Element element = parent.pullElement("GEOCCS"); final String name = element.pullString("name"); final Map<String,?> properties = parseAuthority(element, name); final PrimeMeridian meridian = parsePrimem (element, NonSI.DEGREE_ANGLE); final GeodeticDatum datum = parseDatum (element, meridian); final Unit<Length> linearUnit = parseUnit (element, SI.METER); CoordinateSystemAxis axis0, axis1, axis2; axis0 = parseAxis(element, linearUnit, false); try { if (axis0 != null) { axis1 = parseAxis(element, linearUnit, true); axis2 = parseAxis(element, linearUnit, true); } else { // Those default values are part of WKT specification. axis0 = createAxis(null, "X", AxisDirection.OTHER, linearUnit); axis1 = createAxis(null, "Y", AxisDirection.EAST, linearUnit); axis2 = createAxis(null, "Z", AxisDirection.NORTH, linearUnit); } element.close(); return crsFactory.createGeocentricCRS(properties, datum, csFactory.createCartesianCS(properties, axis0, axis1, axis2)); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses an <strong>optional</strong> "VERT_CS" element. * This element has the following pattern: * * <blockquote><code> * VERT_CS["<name>", <vert datum>, <linear unit>, {<axis>,} {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "VERT_CS" element as a {@link VerticalCRS} object. * @throws ParseException if the "VERT_CS" element can't be parsed. */ private VerticalCRS parseVertCS(final Element parent) throws ParseException { final Element element = parent.pullElement("VERT_CS"); if (element == null) { return null; } String name = element.pullString("name"); VerticalDatum datum = parseVertDatum(element); Unit<Length> linearUnit = parseUnit(element, SI.METER); CoordinateSystemAxis axis = parseAxis(element, linearUnit, false); Map<String,?> properties = parseAuthority(element, name); element.close(); try { if (axis == null) { axis = createAxis(null, "Z", AxisDirection.UP, linearUnit); } return crsFactory.createVerticalCRS(properties, datum, csFactory.createVerticalCS(singletonMap("name", name), axis)); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "GEOGCS" element. This element has the following pattern: * * <blockquote><code> * GEOGCS["<name>", <datum>, <prime meridian>, <angular unit> {,<twin axes>} {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "GEOGCS" element as a {@link GeographicCRS} object. * @throws ParseException if the "GEOGCS" element can't be parsed. */ private GeographicCRS parseGeoGCS(final Element parent) throws ParseException { Element element = parent.pullElement("GEOGCS"); String name = element.pullString("name"); Map<String,?> properties = parseAuthority(element, name); Unit<Angle> angularUnit = parseUnit (element, SI.RADIAN); PrimeMeridian meridian = parsePrimem (element, angularUnit); GeodeticDatum datum = parseDatum (element, meridian); CoordinateSystemAxis axis0 = parseAxis (element, angularUnit, false); CoordinateSystemAxis axis1; try { if (axis0 != null) { axis1 = parseAxis(element, angularUnit, true); } else { // Those default values are part of WKT specification. axis0 = createAxis(null, "Lon", AxisDirection.EAST, angularUnit); axis1 = createAxis(null, "Lat", AxisDirection.NORTH, angularUnit); } element.close(); return crsFactory.createGeographicCRS(properties, datum, csFactory.createEllipsoidalCS(properties, axis0, axis1)); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "PROJCS" element. * This element has the following pattern: * * <blockquote><code> * PROJCS["<name>", <geographic cs>, <projection>, {<parameter>,}*, * <linear unit> {,<twin axes>}{,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "PROJCS" element as a {@link ProjectedCRS} object. * @throws ParseException if the "GEOGCS" element can't be parsed. */ private ProjectedCRS parseProjCS(final Element parent) throws ParseException { Element element = parent.pullElement("PROJCS"); String name = element.pullString("name"); Map<String,?> properties = parseAuthority(element, name); GeographicCRS geoCRS = parseGeoGCS(element); Ellipsoid ellipsoid = geoCRS.getDatum().getEllipsoid(); Unit<Length> linearUnit = parseUnit(element, SI.METER); Unit<Angle> angularUnit = geoCRS.getCoordinateSystem().getAxis(0).getUnit().asType(Angle.class); ParameterValueGroup projection = parseProjection(element, ellipsoid, linearUnit, angularUnit); CoordinateSystemAxis axis0 = parseAxis(element, linearUnit, false); CoordinateSystemAxis axis1; try { if (axis0 != null) { axis1 = parseAxis(element, linearUnit, true); } else { // Those default values are part of WKT specification. axis0 = createAxis(null, "X", AxisDirection.EAST, linearUnit); axis1 = createAxis(null, "Y", AxisDirection.NORTH, linearUnit); } element.close(); final Conversion conversion = new DefiningConversion(name, projection); return crsFactory.createProjectedCRS(properties, geoCRS, conversion, csFactory.createCartesianCS(properties, axis0, axis1)); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "COMPD_CS" element. * This element has the following pattern: * * <blockquote><code> * COMPD_CS["<name>", <head cs>, <tail cs> {,<authority>}] * </code></blockquote> * * @param parent The parent element. * @return The "COMPD_CS" element as a {@link CompoundCRS} object. * @throws ParseException if the "COMPD_CS" element can't be parsed. */ private CompoundCRS parseCompdCS(final Element parent) throws ParseException { final CoordinateReferenceSystem[] CRS = new CoordinateReferenceSystem[2]; Element element = parent.pullElement("COMPD_CS"); String name = element.pullString("name"); Map<String,?> properties = parseAuthority(element, name); CRS[0] = parseCoordinateReferenceSystem(element); CRS[1] = parseCoordinateReferenceSystem(element); element.close(); try { return crsFactory.createCompoundCRS(properties, CRS); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } } /** * Parses a "FITTED_CS" element. * This element has the following pattern: * * <blockquote><code> * FITTED_CS["<name>", <to base>, <base cs>] * </code></blockquote> * * @param parent The parent element. * @return The "FITTED_CS" element as a {@link CompoundCRS} object. * @throws ParseException if the "COMPD_CS" element can't be parsed. */ private DerivedCRS parseFittedCS(final Element parent) throws ParseException { Element element = parent.pullElement("FITTED_CS"); String name = element.pullString("name"); Map<String,?> properties = parseAuthority(element, name); final MathTransform toBase = parseMathTransform(element, true); final CoordinateReferenceSystem base = parseCoordinateReferenceSystem(element); final OperationMethod method = getOperationMethod(); element.close(); /* * WKT provides no informations about the underlying CS of a derived CRS. * We have to guess some reasonable one with arbitrary units. We try to * construct the one which contains as few information as possible, in * order to avoid providing wrong informations. */ final CoordinateSystemAxis[] axis = new CoordinateSystemAxis[toBase.getSourceDimensions()]; final StringBuilder buffer = new StringBuilder(name); buffer.append(" axis "); final int start = buffer.length(); try { for (int i=0; i<axis.length; i++) { final String number = String.valueOf(i); buffer.setLength(start); buffer.append(number); axis[i] = csFactory.createCoordinateSystemAxis( singletonMap(IdentifiedObject.NAME_KEY, buffer.toString()), number, AxisDirection.OTHER, Unit.ONE); } final Conversion conversion = new DefiningConversion( singletonMap(IdentifiedObject.NAME_KEY, method.getName().getCode()), method, toBase.inverse()); final CoordinateSystem cs = new AbstractCS(properties, axis); return crsFactory.createDerivedCRS(properties, base, conversion, cs); } catch (FactoryException exception) { throw element.parseFailed(exception, null); } catch (NoninvertibleTransformException exception) { throw element.parseFailed(exception, null); } } /** * Returns the class of the specified WKT element. For example this method returns * <code>{@linkplain ProjectedCRS}.class</code> for element "{@code PROJCS}". * * @param element The WKT element name. * @return The GeoAPI class of the specified element, or {@code null} if unknow. */ public static Class<?> getClassOf(String element) { if (element == null) { return null; } element = element.trim().toUpperCase(Locale.US); final Class<?> type = getTypeMap().get(element); assert type == null || type.equals(MathTransform.class) || element.equals(getNameOf(type)) : type; return type; } /** * Returns the WKT name of the specified object type. For example this method returns * "{@code PROJCS}" for type <code>{@linkplain ProjectedCRS}.class</code>. * * @param type The GeoAPI class of the specified element. * @return The WKT element name, or {@code null} if unknow. * * @since 2.4 */ public static String getNameOf(final Class<?> type) { if (type != null) { for (final Map.Entry<String,Class<?>> entry : getTypeMap().entrySet()) { final Class<?> candidate = entry.getValue(); if (candidate.isAssignableFrom(type)) { return entry.getKey(); } } } return null; } /** * Returns the type map. */ private static Map<String,Class<?>> getTypeMap() { if (TYPES == null) { final Map<String,Class<?>> map = new LinkedHashMap<String,Class<?>>(25); map.put( "GEOGCS", GeographicCRS.class); map.put( "PROJCS", ProjectedCRS.class); map.put( "GEOCCS", GeocentricCRS.class); map.put( "VERT_CS", VerticalCRS.class); map.put( "LOCAL_CS", EngineeringCRS.class); map.put( "COMPD_CS", CompoundCRS.class); map.put( "FITTED_CS", DerivedCRS.class); map.put( "AXIS", CoordinateSystemAxis.class); map.put( "PRIMEM", PrimeMeridian.class); map.put( "TOWGS84", BursaWolfParameters.class); map.put( "SPHEROID", Ellipsoid.class); map.put( "VERT_DATUM", VerticalDatum.class); map.put( "LOCAL_DATUM", EngineeringDatum.class); map.put( "DATUM", GeodeticDatum.class); map.put( "PARAM_MT", MathTransform.class); map.put( "CONCAT_MT", MathTransform.class); map.put( "INVERSE_MT", MathTransform.class); map.put("PASSTHROUGH_MT", MathTransform.class); TYPES = map; // Sets the field only once completed, in order to avoid synchronisation. // It is not a big deal in current implementation if two Maps are created. } return TYPES; } /** * Read WKT strings from the {@linkplain System#in standard input stream} and * reformat them to the {@linkplain System#out standard output stream}. The * input is read until it reach the end-of-file ({@code [Ctrl-Z]} if * reading from the keyboard), or until an unparsable WKT has been hit. * Optional arguments are: * * <TABLE CELLPADDING='0' CELLSPACING='0'> * <TR><TD NOWRAP><CODE>-authority</CODE> <VAR><name></VAR></TD> * <TD NOWRAP> The authority to prefer when choosing WKT entities names.</TD></TR> * <TR><TD NOWRAP><CODE>-indentation</CODE> <VAR><value></VAR></TD> * <TD NOWRAP> Set the indentation (0 for output on a single line)</TD></TR> * <TR><TD NOWRAP><CODE>-encoding</CODE> <VAR><code></VAR></TD> * <TD NOWRAP> Set the character encoding</TD></TR> * <TR><TD NOWRAP><CODE>-locale</CODE> <VAR><language></VAR></TD> * <TD NOWRAP> Set the language for the output (e.g. "fr" for French)</TD></TR> * </TABLE> * * @param args The command line arguments. */ public static void main(String[] args) { final Arguments arguments = new Arguments(args); final Integer indentation = arguments.getOptionalInteger(Formattable.INDENTATION); final String authority = arguments.getOptionalString("-authority"); args = arguments.getRemainingArguments(0); if (indentation != null) { Formattable.setIndentation(indentation.intValue()); } final BufferedReader in = new BufferedReader(Arguments.getReader(System.in)); try { final Parser parser = new Parser(); if (authority != null) { parser.setAuthority(Citations.fromName(authority)); } parser.reformat(in, arguments.out, arguments.err); } catch (Exception exception) { exception.printStackTrace(arguments.err); } // Do not close 'in', since it is the standard input stream. } }