blob: f5aea8e845e99538e408b396de51e699322aa7d8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.io.wkt;
import java.util.Map;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.text.ParseException;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import javax.measure.Quantity;
import javax.measure.quantity.Time;
import javax.measure.format.ParserException;
import javax.measure.IncommensurableException;
import org.opengis.metadata.Identifier;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.ObjectFactory;
import org.opengis.util.FactoryException;
// 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.apache.sis.measure.Units;
import org.apache.sis.measure.UnitFormat;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.cs.AbstractCS;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.crs.DefaultDerivedCRS;
import org.apache.sis.referencing.datum.BursaWolfParameters;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.internal.referencing.CoordinateOperations;
import org.apache.sis.internal.referencing.Legacy;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.metadata.iso.extent.DefaultGeographicDescription;
import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent;
import org.apache.sis.internal.metadata.AxisNames;
import org.apache.sis.internal.metadata.TransformationAccuracy;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.referencing.EllipsoidalHeightCombiner;
import org.apache.sis.internal.referencing.VerticalDatumTypes;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.Types;
import static java.util.Collections.singletonMap;
/**
* Well Known Text (WKT) parser for referencing objects. This include, but is not limited too,
* {@linkplain org.apache.sis.referencing.crs.AbstractCRS Coordinate Reference System} and
* {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform Math Transform} objects.
* Note that math transforms are part of the WKT 1 {@code "FITTED_CS"} element.
*
* @author Rémi Eve (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
* @version 1.0
* @since 0.6
* @module
*/
class GeodeticObjectParser extends MathTransformParser implements Comparator<CoordinateSystemAxis> {
/**
* The names of the 7 parameters in a {@code TOWGS84[…]} element.
* Those names are derived from the <cite>Well Known Text</cite> (WKT) version 1 specification.
* They are not the same than the {@link org.apache.sis.referencing.datum.BursaWolfParameters}
* field names, which are derived from the EPSG database.
*/
private static final String[] ToWGS84 = {"dx", "dy", "dz", "ex", "ey", "ez", "ppm"};
/**
* During WKT 1 parsing, {@code true} means that {@code PRIMEM} and {@code PARAMETER} angular units
* need to be forced to {@code Units.DEGREE} instead than inferred from the context.
* Note that this rule does not apply to {@code AXIS} elements
*
* <p>This flag is ignored during WKT 2 parsing.</p>
*
* @see Convention#WKT1_COMMON_UNITS
*/
private final boolean usesCommonUnits;
/**
* During WKT 1 parsing, {@code true} means that axes should be parsed only for verifying the syntax,
* but otherwise parsing should behave as if axes were not declared.
*
* <p>This flag is ignored during WKT 2 parsing.</p>
*
* @see Convention#WKT1_IGNORE_AXES
*/
private final boolean ignoreAxes;
/**
* The object to use for replacing WKT axis names and abbreviations by ISO 19111 names and abbreviations.
*/
private final Transliterator transliterator;
/**
* A map of properties to be given to the factory constructor methods.
* This map will be recycled for each object to be parsed.
*/
private final Map<String,Object> properties = new HashMap<>(4);
/**
* Order of coordinate system axes. Used only if {@code AXIS[…]} elements contain {@code ORDER[…]} sub-element.
*/
private final Map<CoordinateSystemAxis,Integer> axisOrder = new IdentityHashMap<>(4);
/**
* The last vertical CRS found during the parsing, or {@code null} if none.
* This information is needed for creating {@link DefaultVerticalExtent} instances.
*
* <p>ISO 19162 said that we should have at most one vertical CRS per WKT. Apache SIS does
* not enforce this constraint, but if a WKT contains more than one vertical CRS then the
* instance used for completing the {@link DefaultVerticalExtent} instances is unspecified.</p>
*/
private transient VerticalCRS verticalCRS;
/**
* A chained list of temporary information needed for completing the construction of {@link DefaultVerticalExtent}
* instances. In particular, stores the unit of measurement until the {@link VerticalCRS} instance to associate to
* the extents become known.
*/
private transient VerticalInfo verticalElements;
/**
* Constructs a parser for the specified set of symbols using the specified set of factories.
*
* This constructor is for internal usage by Apache SIS only — <b>do not use!</b>
*
* <p><b>Maintenance note:</b> this constructor is invoked through reflection by
* {@link org.apache.sis.referencing.factory.GeodeticObjectFactory#createFromWKT(String)}.
* Do not change the method signature even if it doesn't break the compilation, unless the
* reflection code is also updated.</p>
*
* @param defaultProperties default properties to give to the objects to create.
* @param factories an object implementing {@link DatumFactory}, {@link CSFactory} and {@link CRSFactory}.
* @param mtFactory the factory to use to create {@link MathTransform} objects.
*/
public GeodeticObjectParser(final Map<String,?> defaultProperties,
final ObjectFactory factories, final MathTransformFactory mtFactory)
{
super(Symbols.getDefault(), Collections.emptyMap(), null, null, null,
new ReferencingFactoryContainer(defaultProperties,
(CRSFactory) factories,
(CSFactory) factories,
(DatumFactory) factories,
null, mtFactory),
(Locale) defaultProperties.get(Errors.LOCALE_KEY));
transliterator = Transliterator.DEFAULT;
usesCommonUnits = false;
ignoreAxes = false;
}
/**
* Constructs a parser for the specified set of symbols using the specified set of factories.
* This constructor is for {@link WKTFormat} usage only.
*
* @param symbols the set of symbols to use.
* @param fragments reference to the {@link WKTFormat#fragments} map, or an empty map if none.
* @param numberFormat the number format provided by {@link WKTFormat}, or {@code null} for a default format.
* @param dateFormat the date format provided by {@link WKTFormat}, or {@code null} for a default format.
* @param unitFormat the unit format provided by {@link WKTFormat}, or {@code null} for a default format.
* @param convention the WKT convention to use.
* @param errorLocale the locale for error messages (not for parsing), or {@code null} for the system default.
* @param factories on input, the factories to use. On output, the factories used. Can be null.
*/
GeodeticObjectParser(final Symbols symbols, final Map<String,Element> fragments,
final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
final Convention convention, final Transliterator transliterator, final Locale errorLocale,
final ReferencingFactoryContainer factories)
{
super(symbols, fragments, numberFormat, dateFormat, unitFormat, factories, errorLocale);
this.transliterator = transliterator;
usesCommonUnits = convention.usesCommonUnits;
ignoreAxes = convention == Convention.WKT1_IGNORE_AXES;
}
/**
* Returns the name of the class providing the publicly-accessible {@code createFromWKT(String)} method.
* This information is used for logging purpose only.
*/
@Override
String getPublicFacade() {
return "org.apache.sis.referencing.factory.GeodeticObjectFactory";
}
/**
* Parses a <cite>Well Know Text</cite> (WKT).
*
* @param text the text to be parsed.
* @param position the position to start parsing from.
* @return the parsed object.
* @throws ParseException if the string can not be parsed.
*/
@Override
public final Object parseObject(final String text, final ParsePosition position) throws ParseException {
final Object object;
try {
object = super.parseObject(text, position);
/*
* After parsing the object, we may have been unable to set the VerticalCRS of VerticalExtent instances.
* First, try to set a default VerticalCRS for Mean Sea Level Height in metres. In the majority of cases
* that should be enough. If not (typically because the vertical extent uses other unit than metre), try
* to create a new CRS using the unit declared in the WKT.
*/
if (verticalElements != null) {
Exception ex = null;
try {
verticalElements = verticalElements.resolve(CommonCRS.Vertical.MEAN_SEA_LEVEL.crs()); // Optional operation.
} catch (UnsupportedOperationException e) {
ex = e;
}
if (verticalElements != null) try {
verticalElements = verticalElements.complete(factories.getCRSFactory(), factories.getCSFactory());
} catch (FactoryException e) {
if (ex == null) ex = e;
else ex.addSuppressed(e);
}
if (verticalElements != null) {
warning(null, (String) null, Errors.formatInternational(Errors.Keys.CanNotAssignUnitToDimension_2,
WKTKeywords.VerticalExtent, verticalElements.unit), ex);
}
}
} finally {
verticalElements = null;
verticalCRS = null;
axisOrder.clear();
properties.clear(); // for letting the garbage collector do its work.
}
return object;
}
/**
* Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree.
*
* @param element the element to be parsed.
* @return the parsed object.
* @throws ParseException if the element can not be parsed.
*/
@Override
final Object parseObject(final Element element) throws ParseException {
Object value = parseCoordinateReferenceSystem(element, false);
if (value != null) {
return value;
}
value = parseMathTransform(element, false);
if (value != null) {
return value;
}
Object object;
if ((object = parseAxis (FIRST, element, null, Units.METRE )) == null &&
(object = parsePrimeMeridian (FIRST, element, false, Units.DEGREE)) == null &&
(object = parseDatum (FIRST, element, null )) == null &&
(object = parseEllipsoid (FIRST, element )) == null &&
(object = parseToWGS84 (FIRST, element )) == null &&
(object = parseVerticalDatum (FIRST, element, false)) == null &&
(object = parseTimeDatum (FIRST, element )) == null &&
(object = parseParametricDatum (FIRST, element )) == null &&
(object = parseEngineeringDatum (FIRST, element, false)) == null &&
(object = parseImageDatum (FIRST, element )) == null &&
(object = parseOperation (FIRST, element)) == null)
{
throw element.missingOrUnknownComponent(WKTKeywords.GeodeticCRS);
}
return object;
}
/**
* Parses a coordinate reference system element.
*
* @param element the parent element.
* @param mandatory {@code true} if a CRS must be present, or {@code false} if optional.
* @return the next element as a {@code CoordinateReferenceSystem} object.
* @throws ParseException if the next element can not be parsed.
*/
private CoordinateReferenceSystem parseCoordinateReferenceSystem(final Element element, final boolean mandatory)
throws ParseException
{
CoordinateReferenceSystem crs;
if ((crs = parseGeodeticCRS (FIRST, element, 2, null)) == null &&
(crs = parseProjectedCRS (FIRST, element, false)) == null &&
(crs = parseVerticalCRS (FIRST, element, false)) == null &&
(crs = parseTimeCRS (FIRST, element, false)) == null &&
(crs = parseParametricCRS (FIRST, element, false)) == null &&
(crs = parseEngineeringCRS (FIRST, element, false)) == null &&
(crs = parseImageCRS (FIRST, element)) == null &&
(crs = parseCompoundCRS (FIRST, element)) == null &&
(crs = parseFittedCS (FIRST, element)) == null)
{
if (mandatory) {
throw element.missingOrUnknownComponent(WKTKeywords.GeodeticCRS);
}
}
return crs;
}
/**
* Parses a coordinate reference system wrapped in an element of the given name.
*
* @param parent the parent element containing the CRS to parse.
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param keyword "SourceCRS", "TargetCRS" or "InterpolationCRS".
* @return the coordinate reference system, or {@code null} if none.
* @throws ParseException if the CRS can not be parsed.
*/
private CoordinateReferenceSystem parseCoordinateReferenceSystem(final Element parent, final int mode,
final String keyword) throws ParseException
{
final Element element = parent.pullElement(mode, keyword);
if (element == null) {
return null;
}
final CoordinateReferenceSystem crs = parseCoordinateReferenceSystem(element, true);
element.close(ignoredElements);
return crs;
}
/**
* Returns the value associated to {@link IdentifiedObject#IDENTIFIERS_KEY} as an {@code Identifier} object.
* This method shall accept all value types that {@link #parseMetadataAndClose(Element, String, IdentifiedObject)}
* may store.
*
* @param identifier the {@link #properties} value, or {@code null}.
* @return the identifier, or {@code null} if the given value was null.
*/
private static Identifier toIdentifier(final Object identifier) {
return (identifier instanceof Identifier[]) ? ((Identifier[]) identifier)[0] : (Identifier) identifier;
}
/**
* Parses an <strong>optional</strong> metadata elements and close.
* This include elements like {@code "SCOPE"}, {@code "ID"} (WKT 2) or {@code "AUTHORITY"} (WKT 1).
* This WKT 1 element has the following pattern:
*
* {@preformat wkt
* AUTHORITY["<name>", "<code>"]
* }
*
* <h4>Fallback</h4>
* The name is a mandatory property, but some invalid WKT with an empty string exist. In such case,
* we will use the name of the enclosed datum. Indeed, it is not uncommon to have the same name for
* a geographic CRS and its geodetic datum.
*
* @param parent the parent element.
* @param name the name of the parent object being parsed.
* @param fallback the fallback to use if {@code name} is empty.
* @return a properties map with the parent name and the optional authority code.
* @throws ParseException if an element can not be parsed.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private Map<String,Object> parseMetadataAndClose(final Element parent, final String name,
final IdentifiedObject fallback) throws ParseException
{
properties.clear();
properties.put(IdentifiedObject.NAME_KEY, (name.isEmpty() && fallback != null) ? fallback.getName() : name);
Element element;
while ((element = parent.pullElement(OPTIONAL, ID_KEYWORDS)) != null) {
final String codeSpace = element.pullString("codeSpace");
final String code = element.pullObject("code").toString(); // Accepts Integer as well as String.
final Object version = element.pullOptional(Object.class); // Accepts Number as well as String.
final Element citation = element.pullElement(OPTIONAL, WKTKeywords.Citation);
final String authority;
if (citation != null) {
authority = citation.pullString("authority");
citation.close(ignoredElements);
} else {
authority = codeSpace;
}
final Element uri = element.pullElement(OPTIONAL, WKTKeywords.URI);
if (uri != null) {
uri.pullString("URI"); // TODO: not yet stored, since often redundant with other information.
uri.close(ignoredElements);
}
element.close(ignoredElements);
/*
* Note: we could be tempted to assign the authority to the name as well, like below:
*
* if (name instanceof String) {
* name = new NamedIdentifier(authority, (String) name);
* }
* properties.put(IdentifiedObject.NAME_KEY, name);
*
* However experience shows that it is often wrong in practice, because peoples often
* declare EPSG codes but still use WKT names much shorter than the EPSG names
* (for example "WGS84" for the datum instead than "World Geodetic System 1984"),
* so the name in WKT is often not compliant with the name actually defined by the authority.
*/
final ImmutableIdentifier id = new ImmutableIdentifier(Citations.fromName(authority),
codeSpace, code, (version != null) ? version.toString() : null, null);
final Object previous = properties.put(IdentifiedObject.IDENTIFIERS_KEY, id);
if (previous != null) {
Identifier[] identifiers;
if (previous instanceof Identifier) {
identifiers = new Identifier[] {(Identifier) previous, id};
} else {
identifiers = (Identifier[]) previous;
final int n = identifiers.length;
identifiers = Arrays.copyOf(identifiers, n + 1);
identifiers[n] = id;
}
properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifiers);
// REMINDER: values associated to IDENTIFIERS_KEY shall be recognized by 'toIdentifier(Object)'.
}
}
/*
* Other metadata (SCOPE, AREA, etc.). ISO 19162 said that at most one of each type shall be present,
* but our parser accepts an arbitrary amount of some kinds of metadata. They can be recognized by the
* 'while' loop.
*
* Most WKT do not contain any of those metadata, so we perform an 'isEmpty()' check as an optimization
* for those common cases.
*/
if (!parent.isEmpty()) {
/*
* Example: SCOPE["Large scale topographic mapping and cadastre."]
*/
element = parent.pullElement(OPTIONAL, WKTKeywords.Scope);
if (element != null) {
properties.put(ReferenceSystem.SCOPE_KEY, element.pullString("scope")); // Other types like Datum use the same key.
element.close(ignoredElements);
}
/*
* Example: AREA["Netherlands offshore."]
*/
DefaultExtent extent = null;
while ((element = parent.pullElement(OPTIONAL, WKTKeywords.Area)) != null) {
final String area = element.pullString("area");
element.close(ignoredElements);
if (extent == null) {
extent = new DefaultExtent(area, null, null, null);
} else {
extent.getGeographicElements().add(new DefaultGeographicDescription(area));
}
}
/*
* Example: BBOX[51.43, 2.54, 55.77, 6.40]
*/
while ((element = parent.pullElement(OPTIONAL, WKTKeywords.BBox)) != null) {
final double southBoundLatitude = element.pullDouble("southBoundLatitude");
final double westBoundLongitude = element.pullDouble("westBoundLongitude");
final double northBoundLatitude = element.pullDouble("northBoundLatitude");
final double eastBoundLongitude = element.pullDouble("eastBoundLongitude");
element.close(ignoredElements);
if (extent == null) extent = new DefaultExtent();
extent.getGeographicElements().add(new DefaultGeographicBoundingBox(
westBoundLongitude, eastBoundLongitude, southBoundLatitude, northBoundLatitude));
}
/*
* Example: VERTICALEXTENT[-1000, 0, LENGTHUNIT[“metre”, 1]]
*
* Units are optional, default to metres (no "contextual units" here).
*/
while ((element = parent.pullElement(OPTIONAL, WKTKeywords.VerticalExtent)) != null) {
final double minimum = element.pullDouble("minimum");
final double maximum = element.pullDouble("maximum");
Unit<Length> unit = parseScaledUnit(element, WKTKeywords.LengthUnit, Units.METRE);
element.close(ignoredElements);
if (unit == null) unit = Units.METRE;
if (extent == null) extent = new DefaultExtent();
verticalElements = new VerticalInfo(verticalElements, extent, minimum, maximum, unit).resolve(verticalCRS);
}
/*
* Example: TIMEEXTENT[2013-01-01, 2013-12-31]
*
* TODO: syntax like TIMEEXTENT[“Jurassic”, “Quaternary”] is not yet supported.
* See https://issues.apache.org/jira/browse/SIS-163
*
* This operation requires the the sis-temporal module. If not available,
* we will report a warning and leave the temporal extent missing.
*/
while ((element = parent.pullElement(OPTIONAL, WKTKeywords.TimeExtent)) != null) {
if (element.peekValue() instanceof String) {
element.pullString("startTime");
element.pullString("endTime");
element.close(ignoredElements);
warning(parent, element, Errors.formatInternational(Errors.Keys.UnsupportedType_1, "TimeExtent[String,String]"), null);
} else {
final Date startTime = element.pullDate("startTime");
final Date endTime = element.pullDate("endTime");
element.close(ignoredElements);
try {
final DefaultTemporalExtent t = new DefaultTemporalExtent();
t.setBounds(startTime, endTime);
if (extent == null) extent = new DefaultExtent();
extent.getTemporalElements().add(t);
} catch (UnsupportedOperationException e) {
warning(parent, element, null, e);
}
}
}
if (extent != null) {
properties.put(ReferenceSystem.DOMAIN_OF_VALIDITY_KEY, extent);
}
/*
* Example: REMARK["Замечание на русском языке"]
*/
element = parent.pullElement(OPTIONAL, WKTKeywords.Remark);
if (element != null) {
properties.put(IdentifiedObject.REMARKS_KEY, element.pullString("remarks"));
element.close(ignoredElements);
}
}
parent.close(ignoredElements);
return properties;
}
/**
* Parses the datum {@code ANCHOR[]} element and pass the values to the {@link #parseMetadataAndClose(Element,
* String, IdentifiedObject)} method. If an anchor has been found, its value is stored in the returned map.
*/
private Map<String,Object> parseAnchorAndClose(final Element element, final String name) throws ParseException {
final Element anchor = element.pullElement(OPTIONAL, WKTKeywords.Anchor);
final Map<String,Object> properties = parseMetadataAndClose(element, name, null);
if (anchor != null) {
properties.put(Datum.ANCHOR_POINT_KEY, anchor.pullString("anchorDefinition"));
anchor.close(ignoredElements);
}
return properties;
}
/**
* Parses an optional {@code "UNIT"} element of a known dimension.
* This element has the following pattern:
*
* {@preformat wkt
* UNIT["<name>", <conversion factor> {,<authority>}]
* }
*
* Unit was a mandatory element in WKT 1, but became optional in WKT 2 because the unit may be specified
* in each {@code AXIS[…]} element instead than for the whole coordinate system.
*
* @param parent the parent element.
* @param keyword the unit keyword (e.g. {@code "LengthUnit"} or {@code "AngleUnit"}).
* @param baseUnit the base unit, usually {@code Units.METRE} or {@code Units.RADIAN}.
* @return the {@code "UNIT"} element as an {@link Unit} object, or {@code null} if none.
* @throws ParseException if the {@code "UNIT"} can not be parsed.
*
* @see #parseUnit(Element)
*
* @todo Authority code is currently discarded after parsing. We may consider to create a subclass of
* {@link Unit} which implements {@link IdentifiedObject} in a future version.
*/
@SuppressWarnings("unchecked")
private <Q extends Quantity<Q>> Unit<Q> parseScaledUnit(final Element parent,
final String keyword, final Unit<Q> baseUnit) throws ParseException
{
final Element element = parent.pullElement(OPTIONAL, keyword, WKTKeywords.Unit);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final double factor = element.pullDouble("factor");
Unit<Q> unit = baseUnit.multiply(completeUnitFactor(baseUnit, factor));
Unit<?> verify = parseUnitID(element);
element.close(ignoredElements);
/*
* Consider the following element: UNIT[“kilometre”, 1000, ID[“EPSG”, “9036”]]
*
* - if the authority code (“9036”) refers to a unit incompatible with 'baseUnit' (“metre”), log a warning.
* - otherwise: 1) unconditionally replace the parsed unit (“km”) by the unit referenced by the authority code.
* 2) if the new unit is not equivalent to the old one (i.e. different scale factor), log a warning.
*/
if (verify != null) {
if (!baseUnit.getSystemUnit().equals(verify.getSystemUnit())) {
warning(parent, element, Errors.formatInternational(Errors.Keys.InconsistentUnitsForCS_1, verify), null);
} else if (Math.abs(unit.getConverterTo(unit = (Unit<Q>) verify).convert(1) - 1) > Numerics.COMPARISON_THRESHOLD) {
warning(parent, element, Errors.formatInternational(Errors.Keys.UnexpectedScaleFactorForUnit_2, verify, factor), null);
} else {
verify = null; // Means to perform additional verifications.
}
}
/*
* Above block verified the ID[“EPSG”, “9036”] authority code. Now verify the unit parsed from the “km” symbol.
* This is only a verification; we will not replace the unit by the parsed one (i.e. authority code or scale
* factor have precedence over the unit symbol).
*/
if (verify == null) {
try {
verify = parseUnit(name);
} catch (ParserException e) {
log(new LogRecord(Level.FINE, e.toString()));
}
if (verify != null) try {
if (Math.abs(verify.getConverterToAny(unit).convert(1) - 1) > Numerics.COMPARISON_THRESHOLD) {
warning(parent, element, Errors.formatInternational(Errors.Keys.UnexpectedScaleFactorForUnit_2, verify, factor), null);
}
} catch (IncommensurableException e) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.InconsistentUnitsForCS_1,
new Object[] {verify}, element.offset).initCause(e);
}
}
return unit;
}
/**
* Parses a {@code "CS"} element followed by all {@code "AXIS"} elements.
* This element has the following pattern (simplified):
*
* {@preformat wkt
* CS["<type>", dimension],
* AXIS["<name>", NORTH | SOUTH | EAST | WEST | UP | DOWN | OTHER],
* UNIT["<name>", <conversion factor>],
* etc.
* }
*
* This element is different from all other elements parsed by {@code GeodeticObjectParser}
* in that its components are sibling elements rather than child elements of the CS element.
*
* <p>The optional {@code "UNIT[…]"} element shall be parsed by the caller. That element may appear after the
* {@code "CS[…]"} element (not inside). The unit may be forced to some dimension (e.g. {@code "LengthUnit"})
* or be any kind of unit, depending on the context in which this {@code parseCoordinateSystem(…)} method is
* invoked.</p>
*
* <h4>Variants of Cartesian type</h4>
* The {@link WKTKeywords#Cartesian} type may be used for projected, geocentric or other kinds of CRS.
* However while all those variants are of the same CS type, their axis names and directions differ.
* Current implementation uses the following rules:
*
* <ul>
* <li>If the datum is not geodetic, then the axes of the Cartesian CS are unknown.</li>
* <li>Otherwise if {@code dimension is 2}, then the CS is assumed to be for a projected CRS.</li>
* <li>Otherwise if {@code dimension is 3}, then the CS is assumed to be for a geocentric CRS.</li>
* </ul>
*
* @param parent the parent element.
* @param type the expected type (Cartesian | ellipsoidal | vertical | etc…), or null if unknown.
* @param dimension the minimal number of dimensions. Can be 1 if unknown.
* @param isWKT1 {@code true} if the parent element is an element from the WKT 1 standard.
* @param defaultUnit the contextual unit (usually {@code Units.METRE} or {@code Units.RADIAN}), or {@code null} if unknown.
* @param datum the datum of the enclosing CRS, or {@code null} if unknown.
* @return the {@code "CS"}, {@code "UNIT"} and/or {@code "AXIS"} elements as a Coordinate System, or {@code null}.
* @throws ParseException if an element can not be parsed.
* @throws FactoryException if the factory can not create the coordinate system.
*/
private CoordinateSystem parseCoordinateSystem(final Element parent, String type, int dimension,
final boolean isWKT1, final Unit<?> defaultUnit, final Datum datum) throws ParseException, FactoryException
{
axisOrder.clear();
final boolean is3D = (dimension >= 3);
Map<String,Object> csProperties = null;
/*
* Parse the CS[<type>, <dimension>] element. This is specific to the WKT 2 format.
* In principle the CS element is mandatory, but the Apache SIS parser is lenient on
* this aspect: if the CS element is not present, we will compute the same defaults
* than what we do for WKT 1.
*/
if (!isWKT1) {
final Element element = parent.pullElement(OPTIONAL, WKTKeywords.CS);
if (element != null) {
final String expected = type;
type = element.pullVoidElement("type").keyword;
dimension = element.pullInteger("dimension");
csProperties = new HashMap<>(parseMetadataAndClose(element, "CS", null));
if (expected != null) {
if (!expected.equalsIgnoreCase(type)) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.UnexpectedValueInElement_2,
new String[] {WKTKeywords.CS, type}, element.offset);
}
}
if (dimension <= 0 || dimension >= Numerics.MAXIMUM_MATRIX_SIZE) {
final short key;
final Object[] args;
if (dimension <= 0) {
key = Errors.Keys.ValueNotGreaterThanZero_2;
args = new Object[] {"dimension", dimension};
} else {
key = Errors.Keys.ExcessiveNumberOfDimensions_1;
args = new Object[] {dimension};
}
throw new UnparsableObjectException(errorLocale, key, args, element.offset);
}
type = type.equalsIgnoreCase(WKTKeywords.Cartesian) ?
WKTKeywords.Cartesian : type.toLowerCase(symbols.getLocale());
}
}
/*
* AXIS[…] elements are optional, but if we find one we will request that there is as many axes
* as the number of dimensions. If there is more axes than expected, we may emit an error later
* depending on the CS type.
*
* AXIS[…] elements will be parsed for verifying the syntax, but otherwise ignored if the parsing
* convention is WKT1_IGNORE_AXES. This is for compatibility with the way some other libraries
* parse WKT 1.
*/
CoordinateSystemAxis[] axes = null;
CoordinateSystemAxis axis = parseAxis(type == null ? MANDATORY : OPTIONAL, parent, type, defaultUnit);
if (axis != null) {
final List<CoordinateSystemAxis> list = new ArrayList<>(dimension + 2);
do {
list.add(axis);
axis = parseAxis(list.size() < dimension ? MANDATORY : OPTIONAL, parent, type, defaultUnit);
} while (axis != null);
if (!isWKT1 || !ignoreAxes) {
axes = list.toArray(new CoordinateSystemAxis[list.size()]);
Arrays.sort(axes, this); // Take ORDER[n] elements in account.
}
}
/*
* If there is no explicit AXIS[…] elements, or if the user asked to ignore them, then we need to
* create default axes. This is possible only if we know the type of the CS to create, and only
* for some of those CS types.
*/
final CSFactory csFactory = factories.getCSFactory();
if (axes == null) {
if (type == null) {
throw parent.missingComponent(WKTKeywords.Axis);
}
String nx = null, x = null; // Easting or Longitude axis name and abbreviation.
String ny = null, y = null; // Northing or latitude axis name and abbreviation.
String nz = null, z = null; // Depth, height or time axis name and abbreviation.
AxisDirection dx = AxisDirection.EAST;
AxisDirection dy = AxisDirection.NORTH;
AxisDirection direction = null; // Depth, height or time axis direction.
Unit<?> unit = defaultUnit; // Depth, height or time axis unit.
switch (type) {
/*
* Cartesian — we can create axes only for geodetic datum, in which case the axes are for
* two-dimensional Projected or three-dimensional Geocentric CRS.
*/
case WKTKeywords.Cartesian: {
if (!(datum instanceof GeodeticDatum)) {
throw parent.missingComponent(WKTKeywords.Axis);
}
if (defaultUnit == null) {
throw parent.missingComponent(WKTKeywords.LengthUnit);
}
if (is3D) { // If dimension can not be 2, then CRS can not be Projected.
return Legacy.standard(defaultUnit.asType(Length.class));
}
nx = AxisNames.EASTING; x = "E";
ny = AxisNames.NORTHING; y = "N";
if (dimension >= 3) { // Non-standard but SIS is tolerant to this case.
z = "h";
nz = AxisNames.ELLIPSOIDAL_HEIGHT;
unit = Units.METRE;
}
break;
}
/*
* Ellipsoidal — can be two- or three- dimensional, in which case the height can
* only be ellipsoidal height. The default axis order depends on the WKT version:
*
* - WKT 1 said explicitly that the default order is (longitude, latitude).
* - WKT 2 has no default, and allows only (latitude, longitude) order.
*/
case WKTKeywords.ellipsoidal: {
if (defaultUnit == null) {
throw parent.missingComponent(WKTKeywords.AngleUnit);
}
if (isWKT1) {
nx = AxisNames.GEODETIC_LONGITUDE; x = "λ";
ny = AxisNames.GEODETIC_LATITUDE; y = "φ";
} else {
nx = AxisNames.GEODETIC_LATITUDE; x = "φ"; dx = AxisDirection.NORTH;
ny = AxisNames.GEODETIC_LONGITUDE; y = "λ"; dy = AxisDirection.EAST;
}
if (dimension >= 3) {
direction = AxisDirection.UP;
z = "h";
nz = AxisNames.ELLIPSOIDAL_HEIGHT;
unit = Units.METRE;
}
break;
}
/*
* Vertical — the default name and symbol depends on whether this is depth,
* geoidal height, ellipsoidal height (non-standard) or other kind of heights.
*/
case WKTKeywords.vertical: {
if (defaultUnit == null) {
throw parent.missingComponent(WKTKeywords.Unit);
}
z = "h";
nz = "Height";
direction = AxisDirection.UP;
if (datum instanceof VerticalDatum) {
final VerticalDatumType vt = ((VerticalDatum) datum).getVerticalDatumType();
if (vt == VerticalDatumType.GEOIDAL) {
nz = AxisNames.GRAVITY_RELATED_HEIGHT;
z = "H";
} else if (vt == VerticalDatumType.DEPTH) {
direction = AxisDirection.DOWN;
nz = AxisNames.DEPTH;
z = "D";
} else if (vt == VerticalDatumTypes.ELLIPSOIDAL) {
// Not allowed by ISO 19111 as a standalone axis, but SIS is
// tolerant to this case since it is sometime hard to avoid.
nz = AxisNames.ELLIPSOIDAL_HEIGHT;
}
}
break;
}
/*
* Temporal — axis name and abbreviation not yet specified by ISO 19111.
*/
case WKTKeywords.temporal: {
if (defaultUnit == null) {
throw parent.missingComponent(WKTKeywords.TimeUnit);
}
direction = AxisDirection.FUTURE;
nz = "Time";
z = "t";
break;
}
/*
* Parametric — axis name and abbreviation not yet specified by ISO 19111_2.
*/
case WKTKeywords.parametric: {
if (defaultUnit == null) {
throw parent.missingComponent(WKTKeywords.ParametricUnit);
}
direction = AxisDirection.OTHER;
nz = "Parametric";
z = "p";
break;
}
/*
* Unknown CS type — we can not guess which axes to create.
*/
default: {
throw parent.missingComponent(WKTKeywords.Axis);
}
}
int i = 0;
axes = new CoordinateSystemAxis[dimension];
if (x != null && i < dimension) axes[i++] = csFactory.createCoordinateSystemAxis(singletonMap(CoordinateSystemAxis.NAME_KEY, nx), x, dx, defaultUnit);
if (y != null && i < dimension) axes[i++] = csFactory.createCoordinateSystemAxis(singletonMap(CoordinateSystemAxis.NAME_KEY, ny), y, dy, defaultUnit);
if (z != null && i < dimension) axes[i++] = csFactory.createCoordinateSystemAxis(singletonMap(CoordinateSystemAxis.NAME_KEY, nz), z, direction, unit);
// Not a problem if the array does not have the expected length for the CS type. This will be verified below in this method.
}
/*
* Infer a CS name will be inferred from the axes if possible.
* Example: "Compound CS: East (km), North (km), Up (m)."
*/
final String name;
{ // For keeping the 'buffer' variable local to this block.
final StringBuilder buffer = new StringBuilder();
if (type != null && !type.isEmpty()) {
final int c = type.codePointAt(0);
buffer.appendCodePoint(Character.toUpperCase(c))
.append(type, Character.charCount(c), type.length()).append(' ');
}
name = AxisDirections.appendTo(buffer.append("CS"), axes);
}
if (csProperties == null) {
csProperties = singletonMap(CoordinateSystem.NAME_KEY, name);
} else {
csProperties.put(CoordinateSystem.NAME_KEY, name);
}
if (type == null) {
/*
* Creates a coordinate system of unknown type. This block is executed during parsing of WKT version 1,
* since that legacy format did not specified any information about the coordinate system in use.
* This block should not be executed during parsing of WKT version 2.
*/
return new AbstractCS(csProperties, axes);
}
/*
* Finally, delegate to the factory method corresponding to the CS type and the number of axes.
*/
switch (type) {
case WKTKeywords.ellipsoidal: {
switch (axes.length) {
case 2: return csFactory.createEllipsoidalCS(csProperties, axes[0], axes[1]);
case 3: return csFactory.createEllipsoidalCS(csProperties, axes[0], axes[1], axes[2]);
}
dimension = (axes.length < 2) ? 2 : 3; // For error message.
break;
}
case WKTKeywords.Cartesian: {
switch (axes.length) {
case 2: return csFactory.createCartesianCS(csProperties, axes[0], axes[1]);
case 3: return csFactory.createCartesianCS(csProperties, axes[0], axes[1], axes[2]);
}
dimension = (axes.length < 2) ? 2 : 3; // For error message.
break;
}
case WKTKeywords.affine: {
switch (axes.length) {
case 2: return csFactory.createAffineCS(csProperties, axes[0], axes[1]);
case 3: return csFactory.createAffineCS(csProperties, axes[0], axes[1], axes[2]);
}
dimension = (axes.length < 2) ? 2 : 3; // For error message.
break;
}
case WKTKeywords.vertical: {
if (axes.length != (dimension = 1)) break;
return csFactory.createVerticalCS(csProperties, axes[0]);
}
case WKTKeywords.temporal: {
if (axes.length != (dimension = 1)) break;
return csFactory.createTimeCS(csProperties, axes[0]);
}
case WKTKeywords.linear: {
if (axes.length != (dimension = 1)) break;
return csFactory.createLinearCS(csProperties, axes[0]);
}
case WKTKeywords.polar: {
if (axes.length != (dimension = 2)) break;
return csFactory.createPolarCS(csProperties, axes[0], axes[1]);
}
case WKTKeywords.cylindrical: {
if (axes.length != (dimension = 3)) break;
return csFactory.createCylindricalCS(csProperties, axes[0], axes[1], axes[2]);
}
case WKTKeywords.spherical: {
if (axes.length != (dimension = 3)) break;
return csFactory.createSphericalCS(csProperties, axes[0], axes[1], axes[2]);
}
case WKTKeywords.parametric: {
if (axes.length != (dimension = 1)) break;
return csFactory.createParametricCS(csProperties, axes[0]);
}
default: {
warning(parent, WKTKeywords.CS, Errors.formatInternational(Errors.Keys.UnknownType_1, type), null);
return new AbstractCS(csProperties, axes);
}
}
throw new UnparsableObjectException(errorLocale, (axes.length > dimension)
? Errors.Keys.TooManyOccurrences_2 : Errors.Keys.TooFewOccurrences_2,
new Object[] {dimension, WKTKeywords.Axis}, parent.offset);
}
/**
* Parses an {@code "AXIS"} element.
* This element has the following pattern (simplified):
*
* {@preformat wkt
* AXIS["<name (abbr.)>", NORTH | SOUTH | EAST | WEST | UP | DOWN | OTHER, ORDER[n], UNIT[…], ID[…]]
* }
*
* Abbreviation may be specified between parenthesis. Nested parenthesis are possible, as for example:
*
* {@preformat wkt
* AXIS["Easting (E(X))", EAST]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param csType the coordinate system type (Cartesian | ellipsoidal | vertical | etc…), or null if unknown.
* @param defaultUnit the contextual unit (usually {@code Units.METRE} or {@code Units.RADIAN}), or {@code null} if unknown.
* @return the {@code "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 {@code "AXIS"} element can not be parsed.
*/
private CoordinateSystemAxis parseAxis(final int mode, final Element parent, final String csType,
final Unit<?> defaultUnit) throws ParseException
{
final Element element = parent.pullElement(mode, WKTKeywords.Axis);
if (element == null) {
return null;
}
/*
* Name, orientation (usually NORTH, SOUTH, EAST or WEST) and units are the main components of AXIS[…].
* The name may contain an abbreviation, which will be handle later in this method. In the special case
* of coordinate system over a pole, the orientation may be of the form “South along 90°W”, which is
* expressed by a syntax like AXIS[“South along 90°W”, SOUTH, MERIDIAN[-90, UNIT["deg"]]]. Note that
* the meridian is relative to the prime meridian of the enclosing geodetic CRS.
*/
String name = element.pullString("name");
final Element orientation = element.pullVoidElement("orientation");
Unit<?> unit = parseUnit(element);
if (unit == null) {
if (defaultUnit == null) {
throw element.missingComponent(WKTKeywords.Unit);
}
unit = defaultUnit;
}
AxisDirection direction = Types.forCodeName(AxisDirection.class, orientation.keyword, true);
final Element meridian = element.pullElement(OPTIONAL, WKTKeywords.Meridian);
if (meridian != null) {
double angle = meridian.pullDouble("meridian");
final Unit<Angle> m = parseScaledUnit(meridian, WKTKeywords.AngleUnit, Units.RADIAN);
meridian.close(ignoredElements);
if (m != null) {
angle = m.getConverterTo(Units.DEGREE).convert(angle);
}
direction = CoordinateSystems.directionAlongMeridian(direction, angle);
}
/*
* According ISO 19162, the abbreviation should be inserted between parenthesis in the name.
* Example: "Easting (E)", "Longitude (L)". If we do not find an abbreviation, then we will
* have to guess one since abbreviation is a mandatory part of axis.
*/
String abbreviation;
int start, end = name.length() - 1;
if (end > 1 && name.charAt(end) == ')' && (start = name.lastIndexOf('(', end-1)) >= 0) {
// Abbreviation may have nested parenthesis (e.g. "Easting (E(X))").
for (int np = end; (--np >= 0) && name.charAt(np) == ')';) {
final int c = name.lastIndexOf('(', start - 1);
if (c < 0) {
warning(parent, element, Errors.formatInternational(
Errors.Keys.NonEquilibratedParenthesis_2, '(', name), null);
break;
}
start = c;
}
abbreviation = CharSequences.trimWhitespaces(name.substring(start + 1, end));
name = CharSequences.trimWhitespaces(name.substring(0, start));
if (name.isEmpty()) {
name = abbreviation;
}
} else {
abbreviation = AxisDirections.suggestAbbreviation(name, direction, unit);
}
/*
* The longitude and latitude axis names are explicitly fixed by ISO 19111:2007 to "Geodetic longitude"
* and "Geodetic latitude". But ISO 19162:2015 §7.5.3(ii) said that the "Geodetic" part in those names
* shall be omitted at WKT formatting time. SIS's DefaultCoordinateSystemAxis.formatTo(Formatter)
* method performs this removal, so we apply the reverse operation here.
*/
name = transliterator.toLongAxisName (csType, direction, name);
abbreviation = transliterator.toUnicodeAbbreviation(csType, direction, abbreviation);
/*
* At this point we are done and ready to create the CoordinateSystemAxis. But there is one last element
* specified by ISO 19162 but not in Apache SIS representation of axis: ORDER[n], which specify the axis
* ordering. If present we will store that value for processing by the 'parseCoordinateSystem(…)' method.
*/
final Element order = element.pullElement(OPTIONAL, WKTKeywords.Order);
Integer n = null;
if (order != null) {
n = order.pullInteger("order");
order.close(ignoredElements);
}
final CoordinateSystemAxis axis;
final CSFactory csFactory = factories.getCSFactory();
try {
axis = csFactory.createCoordinateSystemAxis(parseMetadataAndClose(element, name, null), abbreviation, direction, unit);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
if (axisOrder.put(axis, n) != null) { // Opportunist check, effective for instances created by SIS factory.
throw new UnparsableObjectException(errorLocale, Errors.Keys.DuplicatedElement_1,
new Object[] {Strings.bracket(WKTKeywords.Axis, name)}, element.offset);
}
return axis;
}
/**
* Compares axes for order. This method is used for ordering axes according their {@code ORDER} element,
* if present. If no {@code ORDER} element were present, then the axis order is left unchanged. If only
* some axes have an {@code ORDER} element (which is illegal according ISO 19162), then those axes will
* be sorted before the axes without {@code ORDER} element.
*
* @param o1 the first axis to compare.
* @param o2 the second axis to compare.
* @return -1 if {@code o1} should be before {@code o2},
* +1 if {@code o2} should be before {@code o1}, or
* 0 if undetermined (no axis order change).
*/
@Override
public final int compare(final CoordinateSystemAxis o1, final CoordinateSystemAxis o2) {
final Integer n1 = axisOrder.get(o1);
final Integer n2 = axisOrder.get(o2);
if (n1 != null) {
if (n2 != null) {
return n1 - n2;
}
return -1; // Axis 1 before Axis 2 since the later has no 'ORDER' element.
} else if (n2 != null) {
return +1; // Axis 2 before Axis 1 since the later has no 'ORDER' element.
}
return 0;
}
/**
* Parses a {@code "PrimeMeridian"} element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#53">WKT 2 specification §8.2.2</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* PRIMEM["<name>", <longitude> {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isWKT1 {@code true} if this method is invoked while parsing a WKT 1 element.
* @param angularUnit the contextual unit.
* @return the {@code "PrimeMeridian"} element as a {@link PrimeMeridian} object.
* @throws ParseException if the {@code "PrimeMeridian"} element can not be parsed.
*
* @see org.apache.sis.referencing.datum.DefaultPrimeMeridian#formatTo(Formatter)
*/
private PrimeMeridian parsePrimeMeridian(final int mode, final Element parent, final boolean isWKT1, Unit<Angle> angularUnit)
throws ParseException
{
if (isWKT1 && usesCommonUnits) {
angularUnit = Units.DEGREE;
}
final Element element = parent.pullElement(mode, WKTKeywords.PrimeMeridian, WKTKeywords.PrimeM);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final double longitude = element.pullDouble("longitude");
final Unit<Angle> unit = parseScaledUnit(element, WKTKeywords.AngleUnit, Units.RADIAN);
if (unit != null) {
angularUnit = unit;
} else if (angularUnit == null) {
throw parent.missingComponent(WKTKeywords.AngleUnit);
}
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createPrimeMeridian(parseMetadataAndClose(element, name, null), longitude, angularUnit);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses an <strong>optional</strong> {@code "TOWGS84"} element.
* This element is specific to WKT 1 and has the following pattern:
*
* {@preformat wkt
* TOWGS84[<dx>, <dy>, <dz>, <ex>, <ey>, <ez>, <ppm>]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "TOWGS84"} element as a {@link org.apache.sis.referencing.datum.BursaWolfParameters} object,
* or {@code null} if no {@code "TOWGS84"} has been found.
* @throws ParseException if the {@code "TOWGS84"} can not be parsed.
*/
private Object parseToWGS84(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.ToWGS84);
if (element == null) {
return null;
}
final double[] values = new double[ToWGS84.length];
for (int i=0; i<values.length;) {
values[i] = element.pullDouble(ToWGS84[i]);
if ((++i % 3) == 0 && element.isEmpty()) {
break; // It is legal to have only 3 or 6 elements.
}
}
element.close(ignoredElements);
final BursaWolfParameters info = new BursaWolfParameters(CommonCRS.WGS84.datum(), null);
info.setValues(values);
return info;
}
/**
* Parses an {@code "Ellipsoid"} element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#52">WKT 2 specification §8.2.1</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* SPHEROID["<name>", <semi-major axis>, <inverse flattening> {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "Ellipsoid"} element as an {@link Ellipsoid} object.
* @throws ParseException if the {@code "Ellipsoid"} element can not be parsed.
*
* @see org.apache.sis.referencing.datum.DefaultEllipsoid#formatTo(Formatter)
*/
private Ellipsoid parseEllipsoid(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.Ellipsoid, WKTKeywords.Spheroid);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final double semiMajorAxis = element.pullDouble("semiMajorAxis");
double inverseFlattening = element.pullDouble("inverseFlattening");
Unit<Length> unit = parseScaledUnit(element, WKTKeywords.LengthUnit, Units.METRE);
if (unit == null) {
unit = Units.METRE;
}
final Map<String,?> properties = parseMetadataAndClose(element, name, null);
final DatumFactory datumFactory = factories.getDatumFactory();
try {
if (inverseFlattening == 0) { // OGC convention for a sphere.
return datumFactory.createEllipsoid(properties, semiMajorAxis, semiMajorAxis, unit);
} else {
return datumFactory.createFlattenedSphere(properties, semiMajorAxis, inverseFlattening, unit);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Returns the number of source dimensions of the given operation method, or 2 if unspecified.
*/
private static int getSourceDimensions(final OperationMethod method) {
if (method != null) {
final Integer dimension = method.getSourceDimensions();
if (dimension != null) {
return dimension;
}
}
return 2;
}
/**
* Parses a {@code "Method"} (WKT 2) element, without the parameters.
*
* @param parent the parent element.
* @param keywords the element keywords.
* @return the operation method.
* @throws ParseException if the {@code "Method"} element can not be parsed.
*/
private OperationMethod parseMethod(final Element parent, final String... keywords) throws ParseException {
final Element element = parent.pullElement(MANDATORY, keywords);
final String name = element.pullString("method");
Map<String,?> properties = parseMetadataAndClose(element, name, null);
final Identifier id = toIdentifier(properties.remove(IdentifiedObject.IDENTIFIERS_KEY)); // See NOTE 2 in parseDerivingConversion.
/*
* The map projection method may be specified by an EPSG identifier (or any other authority),
* which is preferred to the method name since the later is potentially ambiguous. However not
* all CoordinateOperationFactory may accept identifier as an argument to 'getOperationMethod'.
* So if an identifier is present, we will try to use it but fallback on the name if we can
* not use the identifier.
*/
FactoryException suppressed = null;
final CoordinateOperationFactory opFactory = factories.getCoordinateOperationFactory();
if (id != null) try {
// CodeSpace is a mandatory attribute in ID[…] elements, so we do not test for null values.
return opFactory.getOperationMethod(id.getCodeSpace() + Constants.DEFAULT_SEPARATOR + id.getCode());
} catch (FactoryException e) {
suppressed = e;
}
try {
return opFactory.getOperationMethod(name);
} catch (FactoryException e) {
if (suppressed != null) {
e.addSuppressed(suppressed);
}
throw element.parseFailed(e);
}
}
/**
* Parses a {@code "Method"} (WKT 2) element, followed by parameter values. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#62">WKT 2 specification §9.3</a>.
*
* The legacy WKT 1 specification was:
*
* {@preformat wkt
* PROJECTION["<name>" {,<authority>}]
* }
*
* Note that in WKT 2, this element is wrapped inside a {@code Conversion} or {@code DerivingConversion}
* element which is itself inside the {@code ProjectedCRS} element. This is different than WKT 1, which
* puts this element right into the the {@code ProjectedCRS} element without {@code Conversion} wrapper.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param wrapper "Conversion" or "DerivingConversion" wrapper name, or null if parsing a WKT 1.
* @param defaultUnit the unit (usually linear) of the parent element, or {@code null}.
* @param defaultAngularUnit the angular unit of the sibling {@code GeographicCRS} element, or {@code null}.
* @return the {@code "Method"} element and its parameters as a defining conversion.
* @throws ParseException if the {@code "Method"} element can not be parsed.
*/
private Conversion parseDerivingConversion(final int mode, Element parent, final String wrapper,
final Unit<?> defaultUnit, final Unit<Angle> defaultAngularUnit) throws ParseException
{
final String name;
if (wrapper == null) {
name = null; // Will actually be ignored. WKT 1 does not provide name for Conversion objects.
} else {
/*
* If we are parsing WKT 2, then there is an additional "Conversion" element between
* the parent (usually a ProjectedCRS) and the other elements parsed by this method.
*/
parent = parent.pullElement(mode, wrapper);
if (parent == null) {
return null;
}
name = parent.pullString("name");
}
final OperationMethod method = parseMethod(parent, WKTKeywords.Method, WKTKeywords.Projection);
Map<String,?> properties = this.properties; // Same properties then OperationMethod, with ID removed.
/*
* Set the list of parameters.
*
* NOTE 1: Parameters are defined in the parent element (usually a "ProjectedCRS" element
* in WKT 1 or a "Conversion" element in WKT 2), not in this "Method" element.
*
* NOTE 2: We may inherit the OperationMethod name if there is no Conversion wrapper with its own name,
* but we shall not inherit the OperationMethod identifier. This is the reason why we invoked
* properties.remove(IdentifiedObject.IDENTIFIERS_KEY)) above.
*/
final ParameterValueGroup parameters = method.getParameters().createValue();
parseParameters(parent, parameters, defaultUnit, defaultAngularUnit);
if (wrapper != null) {
properties = parseMetadataAndClose(parent, name, method);
/*
* DEPARTURE FROM ISO 19162: the specification in §9.3.2 said:
*
* "If an identifier is provided as an attribute within the <map projection conversion> object,
* because it is expected to describe a complete collection of zone name, method, parameters and
* parameter values, it shall override any identifiers given within the map projection method and
* map projection parameter objects."
*
* However this would require this GeodeticObjectParser to hold a CoordinateOperationAuthorityFactory,
* which we do not yet implement. See https://issues.apache.org/jira/browse/SIS-210
*/
}
final CoordinateOperationFactory opFactory = factories.getCoordinateOperationFactory();
try {
return opFactory.createDefiningConversion(properties, method, parameters);
} catch (FactoryException exception) {
throw parent.parseFailed(exception);
}
}
/**
* Parses a {@code "Datum"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#54">WKT 2 specification §8.2.4</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* DATUM["<name>", <spheroid> {,<to wgs84>} {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param meridian the prime meridian, or {@code null} for Greenwich.
* @return the {@code "Datum"} element as a {@link GeodeticDatum} object.
* @throws ParseException if the {@code "Datum"} element can not be parsed.
*
* @see org.apache.sis.referencing.datum.DefaultGeodeticDatum#formatTo(Formatter)
*/
private GeodeticDatum parseDatum(final int mode, final Element parent, PrimeMeridian meridian) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.Datum, WKTKeywords.GeodeticDatum);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final Ellipsoid ellipsoid = parseEllipsoid(MANDATORY, element);
final Object toWGS84 = parseToWGS84(OPTIONAL, element);
final Map<String,Object> properties = parseAnchorAndClose(element, name);
if (meridian == null) {
meridian = CommonCRS.WGS84.primeMeridian();
}
if (toWGS84 != null) {
properties.put(CoordinateOperations.BURSA_WOLF_KEY, toWGS84);
}
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createGeodeticDatum(properties, ellipsoid, meridian);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "VerticalDatum"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#71">WKT 2 specification §10.2</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* VERT_DATUM["<name>", <datum type> {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isWKT1 {@code true} if the parent is a WKT 1 element.
* @return the {@code "VerticalDatum"} element as a {@link VerticalDatum} object.
* @throws ParseException if the {@code "VerticalDatum"} element can not be parsed.
*/
private VerticalDatum parseVerticalDatum(final int mode, final Element parent, final boolean isWKT1)
throws ParseException
{
final Element element = parent.pullElement(mode,
WKTKeywords.VerticalDatum,
WKTKeywords.VDatum,
WKTKeywords.Vert_Datum);
if (element == null) {
return null;
}
final String name = element.pullString("name");
VerticalDatumType type = null;
if (isWKT1) {
type = VerticalDatumTypes.fromLegacy(element.pullInteger("datum"));
}
if (type == null) {
type = VerticalDatumTypes.guess(name, null, null);
}
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createVerticalDatum(parseAnchorAndClose(element, name), type);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "TimeDatum"} element. This element has the following pattern:
*
* {@preformat wkt
* TimeDatum["<name>", TimeOrigin[<time origin>] {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "TimeDatum"} element as a {@link TemporalDatum} object.
* @throws ParseException if the {@code "TimeDatum"} element can not be parsed.
*/
private TemporalDatum parseTimeDatum(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.TimeDatum, WKTKeywords.TDatum);
if (element == null) {
return null;
}
final String name = element.pullString ("name");
final Element origin = element.pullElement(MANDATORY, WKTKeywords.TimeOrigin);
final Date epoch = origin .pullDate("origin");
origin.close(ignoredElements);
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createTemporalDatum(parseAnchorAndClose(element, name), epoch);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "ParametricDatum"} element. This element has the following pattern:
*
* {@preformat wkt
* ParametricDatum["<name>", Anchor[...] {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "ParametricDatum"} element as a {@link ParametricDatum} object.
* @throws ParseException if the {@code "ParametricDatum"} element can not be parsed.
*/
private ParametricDatum parseParametricDatum(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.ParametricDatum, WKTKeywords.PDatum);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createParametricDatum(parseAnchorAndClose(element, name));
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "EngineeringDatum"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#76">WKT 2 specification §11.2</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* LOCAL_DATUM["<name>", <datum type> {,<authority>}]
* }
*
* The datum type (WKT 1 only) is currently ignored.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isWKT1 {@code true} if the parent is a WKT 1 element.
* @return the {@code "EngineeringDatum"} element as an {@link EngineeringDatum} object.
* @throws ParseException if the {@code "EngineeringDatum"} element can not be parsed.
*/
private EngineeringDatum parseEngineeringDatum(final int mode, final Element parent, final boolean isWKT1) throws ParseException {
final Element element = parent.pullElement(mode,
WKTKeywords.EngineeringDatum,
WKTKeywords.EDatum,
WKTKeywords.Local_Datum);
if (element == null) {
return null;
}
final String name = element.pullString("name");
if (isWKT1) {
element.pullInteger("datum"); // Ignored for now.
}
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createEngineeringDatum(parseAnchorAndClose(element, name));
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses an {@code "ImageDatum"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#81">WKT 2 specification §12.2</a>.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "ImageDatum"} element as an {@link ImageDatum} object.
* @throws ParseException if the {@code "ImageDatum"} element can not be parsed.
*/
private ImageDatum parseImageDatum(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.ImageDatum, WKTKeywords.IDatum);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final PixelInCell pixelInCell = Types.forCodeName(PixelInCell.class,
element.pullVoidElement("pixelInCell").keyword, true);
final DatumFactory datumFactory = factories.getDatumFactory();
try {
return datumFactory.createImageDatum(parseAnchorAndClose(element, name), pixelInCell);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "EngineeringCRS"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#74">WKT 2 specification §11</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* LOCAL_CS["<name>", <local datum>, <unit>, <axis>, {,<axis>}* {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isBaseCRS {@code true} if parsing the CRS inside a {@code DerivedCRS}.
* @return the {@code "EngineeringCRS"} element as an {@link EngineeringCRS} object.
* @throws ParseException if the {@code "EngineeringCRS"} element can not be parsed.
*/
private SingleCRS parseEngineeringCRS(final int mode, final Element parent, final boolean isBaseCRS)
throws ParseException
{
final Element element = parent.pullElement(mode,
isBaseCRS ? new String[] {WKTKeywords.BaseEngCRS} // WKT 2 in DerivedCRS
: new String[] {WKTKeywords.EngineeringCRS, // [0] WKT 2
WKTKeywords.EngCRS, // [1] WKT 2
WKTKeywords.Local_CS}); // [2] WKT 1
if (element == null) {
return null;
}
final boolean isWKT1 = element.getKeywordIndex() == 2; // Index of "Local_CS" above.
final String name = element.pullString("name");
final Unit<?> unit = parseUnit(element);
/*
* An EngineeringCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS.
* In the later case, the datum is null and we have instead DerivingConversion element from a base CRS.
*/
EngineeringDatum datum = null;
SingleCRS baseCRS = null;
Conversion fromBase = null;
if (!isWKT1 && !isBaseCRS) {
/*
* UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
* what to do if they are missing. In this code, we default to the contextual units in the same way than
* what we do for ProjectedCRS parameters, in the hope to be consistent.
*
* An alternative would be to specify null units, in which case MathTransformParser.parseParameters(…)
* defaults to the units specified in the parameter descriptor. But this would make the CRS parser more
* implementation-dependent, because the parameter descriptors are provided by the MathTransformFactory
* instead than inferred from the WKT.
*/
fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, unit, null);
if (fromBase != null) {
/*
* The order of base types below is arbitrary. But no matter their type,
* they must be optional except the last one which should be mandatory.
* The last one determines the error message to be reported if we find none.
*/
baseCRS = parseEngineeringCRS(OPTIONAL, element, true);
if (baseCRS == null) {
baseCRS = parseGeodeticCRS(OPTIONAL, element, getSourceDimensions(fromBase.getMethod()), WKTKeywords.ellipsoidal);
if (baseCRS == null) {
baseCRS = parseProjectedCRS(MANDATORY, element, true);
}
}
}
}
if (baseCRS == null) { // The most usual case.
datum = parseEngineeringDatum(MANDATORY, element, isWKT1);
}
final CRSFactory crsFactory = factories.getCRSFactory();
try {
final CoordinateSystem cs = parseCoordinateSystem(element, null, 1, isWKT1, unit, datum);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (baseCRS != null) {
return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
}
return crsFactory.createEngineeringCRS(properties, datum, cs);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses an {@code "ImageCRS"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#79">WKT 2 specification §12</a>.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "ImageCRS"} element as an {@link ImageCRS} object.
* @throws ParseException if the {@code "ImageCRS"} element can not be parsed.
*/
private ImageCRS parseImageCRS(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.ImageCRS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final ImageDatum datum = parseImageDatum(MANDATORY, element);
final Unit<?> unit = parseUnit(element);
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, false, unit, datum);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (cs instanceof AffineCS) {
return factories.getCRSFactory().createImageCRS(properties, datum, (AffineCS) cs);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses a {@code "GeodeticCRS"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#49">WKT 2 specification §8</a>.
*
* The legacy WKT 1 specification had two elements for this:
*
* {@preformat wkt
* GEOGCS["<name>", <datum>, <prime meridian>, <angular unit> {,<twin axes>} {,<authority>}]
* }
*
* and
*
* {@preformat wkt
* GEOCCS["<name>", <datum>, <prime meridian>, <linear unit> {,<axis> ,<axis> ,<axis>} {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param dimension the minimal number of dimensions (usually 2).
* @param csType the default coordinate system type, or {@code null} if unknown.
* Should be non-null only when parsing a {@link GeneralDerivedCRS#getBaseCRS()} component.
* @return the {@code "GeodeticCRS"} element as a {@link GeographicCRS} or {@link GeocentricCRS} object.
* @throws ParseException if the {@code "GeodeticCRS"} element can not be parsed.
*
* @see org.apache.sis.referencing.crs.DefaultGeographicCRS#formatTo(Formatter)
* @see org.apache.sis.referencing.crs.DefaultGeocentricCRS#formatTo(Formatter)
*/
private SingleCRS parseGeodeticCRS(final int mode, final Element parent, int dimension, String csType)
throws ParseException
{
final Element element = parent.pullElement(mode,
(csType != null) ? new String[] {WKTKeywords.BaseGeodCRS, // [0] WKT 2 in ProjectedCRS or DerivedCRS
WKTKeywords.GeogCS} // [1] WKT 1 in ProjectedCRS
: new String[] {WKTKeywords.GeodeticCRS, // [0] WKT 2
WKTKeywords.GeogCS, // [1] WKT 1
WKTKeywords.GeodCRS, // [2] WKT 2
WKTKeywords.GeocCS}); // [3] WKT 1
if (element == null) {
return null;
}
final boolean isWKT1;
Unit<?> csUnit;
final Unit<Angle> angularUnit;
switch (element.getKeywordIndex()) {
default: {
/*
* WKT 2 "GeodeticCRS" element.
* The specification in §8.2.2 (ii) said:
*
* "If the subtype of the geodetic CRS to which the prime meridian is an attribute
* is geographic, the prime meridian’s <irm longitude> value shall be given in the
* same angular units as those for the horizontal axes of the geographic CRS;
* if the geodetic CRS subtype is geocentric the prime meridian’s <irm longitude>
* value shall be given in degrees."
*
* An apparent ambiguity exists for Geocentric CRS using a Spherical CS instead than the more
* usual Cartesian CS: despite using angular units, we should not use the result of parseUnit
* for those CRS. However this ambiguity should not happen in practice because such Spherical
* CS have a third axis in metre. Since the unit is not the same for all axes, csUnit should
* be null if the WKT is well-formed.
*/
isWKT1 = false;
csUnit = parseUnit(element);
if (Units.isAngular(csUnit)) {
angularUnit = csUnit.asType(Angle.class);
} else {
angularUnit = Units.DEGREE;
if (csUnit == null) {
/*
* A UNIT[…] is mandatory either in the CoordinateSystem as a whole (csUnit != null),
* or inside each AXIS[…] component (csUnit == null). An exception to this rule is when
* parsing a BaseGeodCRS inside a ProjectedCRS or DerivedCRS, in which case axes are omitted.
* We recognize those cases by a non-null 'csType' given in argument to this method.
*/
if (WKTKeywords.ellipsoidal.equals(csType)) {
csUnit = Units.DEGREE; // For BaseGeodCRS in ProjectedCRS.
}
}
}
break;
}
case 1: {
/*
* WKT 1 "GeogCS" (Geographic) element.
*/
isWKT1 = true;
csType = WKTKeywords.ellipsoidal;
angularUnit = parseScaledUnit(element, WKTKeywords.AngleUnit, Units.RADIAN);
csUnit = angularUnit;
dimension = 2;
break;
}
case 3: {
/*
* WKT 1 "GeocCS" (Geocentric) element.
*/
isWKT1 = true;
csType = WKTKeywords.Cartesian;
angularUnit = Units.DEGREE;
csUnit = parseScaledUnit(element, WKTKeywords.LengthUnit, Units.METRE);
dimension = 3;
break;
}
}
final String name = element.pullString("name");
/*
* A GeodeticCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind GeodeticCRS.
* In the later case, the datum is null and we have instead DerivingConversion element from a BaseGeodCRS.
*/
SingleCRS baseCRS = null;
Conversion fromBase = null;
if (!isWKT1 && csType == null) {
/*
* UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
* what to do if they are missing. In this code, we default to the contextual units in the same way than
* what we do for ProjectedCRS parameters, in the hope to be consistent.
*
* An alternative would be to specify null units, in which case MathTransformParser.parseParameters(…)
* defaults to the units specified in the parameter descriptor. But this would make the CRS parser more
* implementation-dependent, because the parameter descriptors are provided by the MathTransformFactory
* instead than inferred from the WKT.
*/
fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, csUnit, angularUnit);
if (fromBase != null) {
baseCRS = parseGeodeticCRS(MANDATORY, element, getSourceDimensions(fromBase.getMethod()), WKTKeywords.ellipsoidal);
}
}
/*
* At this point, we have either a non-null 'datum' or non-null 'baseCRS' + 'fromBase'.
* The coordinate system is parsed in the same way for both cases, but the CRS is created differently.
*/
final CRSFactory crsFactory = factories.getCRSFactory();
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, csType, dimension, isWKT1, csUnit, null);
if (baseCRS != null) {
final Map<String,?> properties = parseMetadataAndClose(element, name, null);
return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
}
/*
* The specification in §8.2.2 (ii) said:
*
* "(snip) the prime meridian’s <irm longitude> value shall be given in the
* same angular units as those for the horizontal axes of the geographic CRS."
*
* This is a little bit different than using the 'angularUnit' variable directly,
* since the WKT could have overwritten the unit directly in the AXIS[…] element.
* So we re-fetch the angular unit. Normally, we will get the same value (unless
* the previous value was null).
*/
final Unit<Angle> longitudeUnit = AxisDirections.getAngularUnit(cs, angularUnit);
if (angularUnit != null && !angularUnit.equals(longitudeUnit)) {
warning(element, WKTKeywords.AngleUnit, Errors.formatInternational(
Errors.Keys.InconsistentUnitsForCS_1, angularUnit), null);
}
final PrimeMeridian meridian = parsePrimeMeridian(OPTIONAL, element, isWKT1, longitudeUnit);
final GeodeticDatum datum = parseDatum(MANDATORY, element, meridian);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (cs instanceof EllipsoidalCS) { // By far the most frequent case.
return crsFactory.createGeographicCRS(properties, datum, (EllipsoidalCS) cs);
}
if (cs instanceof CartesianCS) { // The second most frequent case.
return crsFactory.createGeocentricCRS(properties, datum,
Legacy.forGeocentricCRS((CartesianCS) cs, false));
}
if (cs instanceof SphericalCS) { // Not very common case.
return crsFactory.createGeocentricCRS(properties, datum, (SphericalCS) cs);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses a {@code "VerticalCRS"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#69">WKT 2 specification §10</a>.
*
* The legacy WKT 1 pattern was:
*
* {@preformat wkt
* VERT_CS["<name>", <vert datum>, <linear unit>, {<axis>,} {,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isBaseCRS {@code true} if parsing the CRS inside a {@code DerivedCRS}.
* @return the {@code "VerticalCRS"} element as a {@link VerticalCRS} object.
* @throws ParseException if the {@code "VerticalCRS"} element can not be parsed.
*/
@SuppressWarnings("null")
private SingleCRS parseVerticalCRS(final int mode, final Element parent, final boolean isBaseCRS)
throws ParseException
{
final Element element = parent.pullElement(mode,
isBaseCRS ? new String[] {WKTKeywords.BaseVertCRS} // WKT 2 in DerivedCRS
: new String[] {WKTKeywords.VerticalCRS, // [0] WKT 2
WKTKeywords.VertCRS, // [1] WKT 2
WKTKeywords.Vert_CS}); // [2] WKT 1
if (element == null) {
return null;
}
final boolean isWKT1 = element.getKeywordIndex() == 2; // Index of "Vert_CS" above.
final String name = element.pullString("name");
final Unit<?> unit = parseUnit(element);
/*
* A VerticalCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind VerticalCRS.
* In the later case, the datum is null and we have instead DerivingConversion element from a BaseVertCRS.
*/
VerticalDatum datum = null;
SingleCRS baseCRS = null;
Conversion fromBase = null;
if (!isWKT1 && !isBaseCRS) {
/*
* UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
* what to do if they are missing. In this code, we default to the contextual units in the same way than
* what we do for ProjectedCRS parameters, in the hope to be consistent.
*
* An alternative would be to specify null units, in which case MathTransformParser.parseParameters(…)
* defaults to the units specified in the parameter descriptor. But this would make the CRS parser more
* implementation-dependent, because the parameter descriptors are provided by the MathTransformFactory
* instead than inferred from the WKT.
*/
fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, unit, null);
if (fromBase != null) {
baseCRS = parseVerticalCRS(MANDATORY, element, true);
}
}
if (baseCRS == null) { // The most usual case.
datum = parseVerticalDatum(MANDATORY, element, isWKT1);
}
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, WKTKeywords.vertical, 1, isWKT1, unit, datum);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (cs instanceof VerticalCS) {
final CRSFactory crsFactory = factories.getCRSFactory();
if (baseCRS != null) {
return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
}
/*
* The 'parseVerticalDatum(…)' method may have been unable to resolve the datum type.
* But sometime the axis (which was not available when we created the datum) provides
* more information. Verify if we can have a better type now, and if so rebuild the datum.
*/
if (VerticalDatumType.OTHER_SURFACE.equals(datum.getVerticalDatumType())) {
final VerticalDatumType type = VerticalDatumTypes.guess(datum.getName().getCode(), datum.getAlias(), cs.getAxis(0));
if (!VerticalDatumType.OTHER_SURFACE.equals(type)) {
final DatumFactory datumFactory = factories.getDatumFactory();
datum = datumFactory.createVerticalDatum(IdentifiedObjects.getProperties(datum), type);
}
}
verticalCRS = crsFactory.createVerticalCRS(properties, datum, (VerticalCS) cs);
/*
* Some DefaultVerticalExtent objects may be waiting for the VerticalCRS before to complete
* their construction. If this is the case, try to complete them now.
*/
if (verticalElements != null) {
verticalElements = verticalElements.resolve(verticalCRS);
}
return verticalCRS;
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses {@code "TimeCRS"} element.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isBaseCRS {@code true} if parsing the CRS inside a {@code DerivedCRS}.
* @return the {@code "TimeCRS"} element as a {@link TemporalCRS} object.
* @throws ParseException if the {@code "TimeCRS"} element can not be parsed.
*/
private SingleCRS parseTimeCRS(final int mode, final Element parent, final boolean isBaseCRS)
throws ParseException
{
final Element element = parent.pullElement(mode, isBaseCRS ? WKTKeywords.BaseTimeCRS : WKTKeywords.TimeCRS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final Unit<Time> unit = parseScaledUnit(element, WKTKeywords.TimeUnit, Units.SECOND);
/*
* A TemporalCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind TemporalCRS.
* In the later case, the datum is null and we have instead DerivingConversion element from a BaseTimeCRS.
*/
TemporalDatum datum = null;
SingleCRS baseCRS = null;
Conversion fromBase = null;
if (!isBaseCRS) {
/*
* UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
* what to do if they are missing. In this code, we default to the contextual units in the same way than
* what we do for ProjectedCRS parameters, in the hope to be consistent.
*
* An alternative would be to specify null units, in which case MathTransformParser.parseParameters(…)
* defaults to the units specified in the parameter descriptor. But this would make the CRS parser more
* implementation-dependent, because the parameter descriptors are provided by the MathTransformFactory
* instead than inferred from the WKT.
*/
fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, unit, null);
if (fromBase != null) {
baseCRS = parseTimeCRS(MANDATORY, element, true);
}
}
if (baseCRS == null) { // The most usual case.
datum = parseTimeDatum(MANDATORY, element);
}
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, WKTKeywords.temporal, 1, false, unit, datum);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (cs instanceof TimeCS) {
final CRSFactory crsFactory = factories.getCRSFactory();
if (baseCRS != null) {
return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
}
return crsFactory.createTemporalCRS(properties, datum, (TimeCS) cs);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses {@code "ParametricCRS"} element.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isBaseCRS {@code true} if parsing the CRS inside a {@code DerivedCRS}.
* @return the {@code "ParametricCRS"} object.
* @throws ParseException if the {@code "ParametricCRS"} element can not be parsed.
*/
private SingleCRS parseParametricCRS(final int mode, final Element parent, final boolean isBaseCRS)
throws ParseException
{
final Element element = parent.pullElement(mode, isBaseCRS ? WKTKeywords.BaseParamCRS : WKTKeywords.ParametricCRS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final Unit<?> unit = parseUnit(element);
/*
* A ParametricCRS can be either a "normal" one (with a non-null datum), or a DerivedCRS of kind ParametricCRS.
* In the later case, the datum is null and we have instead DerivingConversion element from a BaseParametricCRS.
*/
ParametricDatum datum = null;
SingleCRS baseCRS = null;
Conversion fromBase = null;
if (!isBaseCRS) {
/*
* UNIT[…] in DerivedCRS parameters are mandatory according ISO 19162 and the specification does not said
* what to do if they are missing. In this code, we default to the contextual units in the same way than
* what we do for ProjectedCRS parameters, in the hope to be consistent.
*
* An alternative would be to specify null units, in which case MathTransformParser.parseParameters(…)
* defaults to the units specified in the parameter descriptor. But this would make the CRS parser more
* implementation-dependent, because the parameter descriptors are provided by the MathTransformFactory
* instead than inferred from the WKT.
*/
fromBase = parseDerivingConversion(OPTIONAL, element, WKTKeywords.DerivingConversion, unit, null);
if (fromBase != null) {
baseCRS = parseParametricCRS(MANDATORY, element, true);
}
}
if (baseCRS == null) { // The most usual case.
datum = parseParametricDatum(MANDATORY, element);
}
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, WKTKeywords.parametric, 1, false, unit, datum);
final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
if (cs instanceof ParametricCS) {
final CRSFactory crsFactory = factories.getCRSFactory();
if (baseCRS != null) {
return crsFactory.createDerivedCRS(properties, baseCRS, fromBase, cs);
}
return crsFactory.createParametricCRS(properties, datum, (ParametricCS) cs);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses a {@code "ProjectedCRS"} (WKT 2) element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#57">WKT 2 specification §9</a>.
*
* The legacy WKT 1 specification was:
*
* {@preformat wkt
* PROJCS["<name>", <geographic cs>, <projection>, {<parameter>,}*,
* <linear unit> {,<twin axes>}{,<authority>}]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @param isBaseCRS {@code true} if parsing the CRS inside a {@code DerivedCRS}.
* @return the {@code "ProjectedCRS"} element as a {@link ProjectedCRS} object.
* @throws ParseException if the {@code "ProjectedCRS"} element can not be parsed.
*/
private ProjectedCRS parseProjectedCRS(final int mode, final Element parent, final boolean isBaseCRS)
throws ParseException
{
final Element element = parent.pullElement(mode,
isBaseCRS ? new String[] {WKTKeywords.BaseProjCRS} // WKT 2 in DerivedCRS
: new String[] {WKTKeywords.ProjectedCRS, // [0] WKT 2
WKTKeywords.ProjCRS, // [1] WKT 2
WKTKeywords.ProjCS}); // [2] WKT 1
if (element == null) {
return null;
}
final boolean isWKT1 = element.getKeywordIndex() == 2; // Index of "ProjCS" above.
final String name = element.pullString("name");
final SingleCRS geoCRS = parseGeodeticCRS(MANDATORY, element, 2, WKTKeywords.ellipsoidal);
if (!(geoCRS instanceof GeographicCRS)) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.IllegalCRSType_1,
new Object[] {geoCRS.getClass()}, element.offset);
}
/*
* Parse the projection parameters. If a default linear unit is specified, it will apply to
* all parameters that do not specify explicitly a LengthUnit. If no such crs-wide unit was
* specified, then the default will be degrees.
*
* More specifically §9.3.4 in the specification said about the default units:
*
* - lengths shall be given in the unit for the projected CRS axes.
* - angles shall be given in the unit for the base geographic CRS of the projected CRS.
*/
Unit<Length> csUnit = parseScaledUnit(element, WKTKeywords.LengthUnit, Units.METRE);
final Unit<Length> linearUnit;
final Unit<Angle> angularUnit;
if (isWKT1 && usesCommonUnits) {
linearUnit = Units.METRE;
angularUnit = Units.DEGREE;
} else {
linearUnit = csUnit;
angularUnit = AxisDirections.getAngularUnit(geoCRS.getCoordinateSystem(), Units.DEGREE);
}
final Conversion conversion = parseDerivingConversion(MANDATORY, element,
isWKT1 ? null : WKTKeywords.Conversion, linearUnit, angularUnit);
/*
* Parse the coordinate system. The linear unit must be specified somewhere, either explicitly in each axis
* or for the whole CRS with the above 'csUnit' value. If 'csUnit' is null, then an exception will be thrown
* with a message like "A LengthUnit component is missing in ProjectedCRS".
*
* However we make an exception if we are parsing a BaseProjCRS, since the coordinate system is unspecified
* in the WKT of base CRS. In this case only, we will default to metre.
*/
if (csUnit == null && isBaseCRS) {
csUnit = Units.METRE;
}
final CoordinateSystem cs;
try {
cs = parseCoordinateSystem(element, WKTKeywords.Cartesian, 2, isWKT1, csUnit, geoCRS.getDatum());
final Map<String,?> properties = parseMetadataAndClose(element, name, conversion);
if (cs instanceof CartesianCS) {
final CRSFactory crsFactory = factories.getCRSFactory();
return crsFactory.createProjectedCRS(properties, (GeographicCRS) geoCRS, conversion, (CartesianCS) cs);
}
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
throw element.illegalCS(cs);
}
/**
* Parses a {@code "CompoundCRS"} element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#110">WKT 2 specification §16</a>.
*
* The legacy WKT 1 specification was:
*
* {@preformat wkt
* COMPD_CS["<name>", <head cs>, <tail cs> {,<authority>}]
* }
*
* In the particular case where there is a geographic CRS and an ellipsoidal height,
* this method rather build a three-dimensional geographic CRS.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "CompoundCRS"} element as a {@link CompoundCRS} object.
* @throws ParseException if the {@code "CompoundCRS"} element can not be parsed.
*/
private CoordinateReferenceSystem parseCompoundCRS(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.CompoundCRS, WKTKeywords.Compd_CS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
CoordinateReferenceSystem crs;
final List<CoordinateReferenceSystem> components = new ArrayList<>(4);
while ((crs = parseCoordinateReferenceSystem(element, components.size() < 2)) != null) {
components.add(crs);
}
try {
return new EllipsoidalHeightCombiner(factories).createCompoundCRS(
parseMetadataAndClose(element, name, null),
components.toArray(new CoordinateReferenceSystem[components.size()]));
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "FITTED_CS"} element.
* This element has the following pattern:
*
* {@preformat wkt
* FITTED_CS["<name>", <to base>, <base cs>]
* }
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "FITTED_CS"} element as a {@link CompoundCRS} object.
* @throws ParseException if the {@code "COMPD_CS"} element can not be parsed.
*/
private DerivedCRS parseFittedCS(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.Fitted_CS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final MathTransform toBase = parseMathTransform(element, true);
final OperationMethod method = getOperationMethod();
final CoordinateReferenceSystem baseCRS = parseCoordinateReferenceSystem(element, true);
if (!(baseCRS instanceof SingleCRS)) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.UnexpectedValueInElement_2,
new Object[] {WKTKeywords.Fitted_CS, baseCRS.getClass()}, element.offset);
}
/*
* WKT 1 provides no information 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 information.
*/
final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[toBase.getSourceDimensions()];
final StringBuilder buffer = new StringBuilder(name).append(" axis ");
final int start = buffer.length();
final CSFactory csFactory = factories.getCSFactory();
try {
for (int i=0; i<axes.length; i++) {
final String number = String.valueOf(i);
buffer.setLength(start);
buffer.append(number);
axes[i] = csFactory.createCoordinateSystemAxis(
singletonMap(CoordinateSystemAxis.NAME_KEY, buffer.toString()),
number, AxisDirection.OTHER, Units.UNITY);
}
final Map<String,Object> properties = parseMetadataAndClose(element, name, baseCRS);
final CoordinateSystem derivedCS = new AbstractCS(
singletonMap(CoordinateSystem.NAME_KEY, AxisDirections.appendTo(new StringBuilder("CS"), axes)), axes);
/*
* Creates a derived CRS from the information found in a WKT 1 {@code FITTED_CS} element.
* This coordinate system can not be easily constructed from the information provided by
* the WKT 1 format, which block us from using the standard Coordinate System factory.
* Note that we do not know which name to give to the conversion method; for now we use the CRS name.
*/
properties.put("conversion.name", name);
return DefaultDerivedCRS.create(properties, (SingleCRS) baseCRS, null, method, toBase.inverse(), derivedCS);
} catch (FactoryException | NoninvertibleTransformException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "CoordinateOperation"} element. The syntax is given by
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#113">WKT 2 specification §17</a>.
*
* @param mode {@link #FIRST}, {@link #OPTIONAL} or {@link #MANDATORY}.
* @param parent the parent element.
* @return the {@code "CoordinateOperation"} element as a {@link CoordinateOperation} object.
* @throws ParseException if the {@code "CoordinateOperation"} element can not be parsed.
*/
private CoordinateOperation parseOperation(final int mode, final Element parent) throws ParseException {
final Element element = parent.pullElement(mode, WKTKeywords.CoordinateOperation);
if (element == null) {
return null;
}
final String name = element.pullString("name");
final CoordinateReferenceSystem sourceCRS = parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.SourceCRS);
final CoordinateReferenceSystem targetCRS = parseCoordinateReferenceSystem(element, MANDATORY, WKTKeywords.TargetCRS);
final CoordinateReferenceSystem interpolationCRS = parseCoordinateReferenceSystem(element, OPTIONAL, WKTKeywords.InterpolationCRS);
final OperationMethod method = parseMethod(element, WKTKeywords.Method);
final Element accuracy = element.pullElement(OPTIONAL, WKTKeywords.OperationAccuracy);
final Map<String,Object> properties = parseMetadataAndClose(element, name, method);
final ParameterValueGroup parameters = method.getParameters().createValue();
parseParameters(element, parameters, null, null);
properties.put(CoordinateOperations.PARAMETERS_KEY, parameters);
if (accuracy != null) {
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY,
TransformationAccuracy.create(accuracy.pullDouble("accuracy")));
accuracy.close(ignoredElements);
}
try {
final DefaultCoordinateOperationFactory df;
final CoordinateOperationFactory opFactory = factories.getCoordinateOperationFactory();
if (opFactory instanceof DefaultCoordinateOperationFactory) {
df = (DefaultCoordinateOperationFactory) opFactory;
} else {
df = CoordinateOperations.factory();
}
return df.createSingleOperation(properties, sourceCRS, targetCRS, interpolationCRS, method, null);
} catch (FactoryException e) {
throw element.parseFailed(e);
}
}
}