blob: fd0711f8b6e9cd4e25046ac29e500b284bef3bae [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.Collections;
import java.util.Arrays;
import java.util.Locale;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import javax.measure.format.ParserException;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.referencing.operation.SingleOperation;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.OperationMethod;
import org.apache.sis.internal.referencing.CoordinateOperations;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.measure.UnitFormat;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Well Known Text (WKT) parser for {@linkplain MathTransform math transform}s.
* Note that while this base class is restricted to math transforms, subclasses may parse a wider range of objects.
*
* @author RĂ©mi Eve (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Rueben Schulz (UBC)
* @version 1.0
*
* @see <a href="http://www.geoapi.org/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well Know Text specification</a>
*
* @since 0.6
* @module
*/
class MathTransformParser extends AbstractParser {
/**
* The keywords for {@code ID} or {@code AUTHORITY} elements, as a static array because frequently used.
*/
static final String[] ID_KEYWORDS = {WKTKeywords.Id, WKTKeywords.Authority};
/**
* The keywords of unit elements. Most frequently used keywords should be first.
*/
private static final String[] UNIT_KEYWORDS = {
WKTKeywords.Unit, // Ignored since it does not allow us to know the quantity dimension.
WKTKeywords.LengthUnit, WKTKeywords.AngleUnit, WKTKeywords.ScaleUnit, WKTKeywords.TimeUnit,
WKTKeywords.ParametricUnit // Ignored for the same reason than "Unit".
};
/**
* The base units associated to the {@link #UNIT_KEYWORDS}, ignoring {@link WKTKeywords#Unit}.
* For each {@code UNIT_KEYWORDS[i]} element, the associated base unit is {@code BASE_UNIT[i-1]}.
*/
private static final Unit<?>[] BASE_UNITS = {
Units.METRE, Units.RADIAN, Units.UNITY, Units.SECOND
};
/**
* Some conversion factors applied to {@link #UNIT_KEYWORDS} for which rounding errors are found in practice.
* Some Well Known Texts define factors with low accuracy, as in {@code ANGLEUNIT["degree", 0.01745329252]}.
* This causes the parser to fail to recognize that the unit is degree and to convert angles with that factor.
* This may result in surprising behavior like <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>.
* This array is a workaround for that problem, adding the missing accuracy to factors.
* This workaround should be removed in a future version if we fix
* <a href="https://issues.apache.org/jira/browse/SIS-433">SIS-433</a>.
*
* <p>Values in each array <strong>must</strong> be sorted in ascending order.</p>
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>
* @see <a href="https://issues.apache.org/jira/browse/SIS-433">SIS-433</a>
*/
private static final double[][] CONVERSION_FACTORS = {
{0.3048, // foot, declared for avoiding that unit to be confused with US survey foot.
0.30480060960121924, // US survey foot
1609.3472186944375}, // US survey mile
{Math.PI/(180*60*60), // Arc-second: 4.84813681109536E-6
Math.PI/(180*60), // Arc-minute: 2.908882086657216E-4
Math.PI/(200), // Grad: 1.5707963267948967E-2
Math.PI/(180)} // Degree: 1.7453292519943295E-2
};
/**
* The factories to use for creating math transforms and geodetic objects.
*/
final ReferencingFactoryContainer factories;
/**
* The classification of the last math transform or projection parsed, or {@code null} if none.
*/
private transient String classification;
/**
* The method for the last math transform passed, or {@code null} if none.
*
* @see #getOperationMethod()
*/
private transient OperationMethod lastMethod;
/**
* Creates a parser for the given factory.
*
* <p><b>Maintenance note:</b> this constructor is invoked through reflection by
* {@link org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#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 mtFactory the factory to use for creating {@link MathTransform} objects.
*/
public MathTransformParser(final MathTransformFactory mtFactory) {
this(Symbols.getDefault(), Collections.emptyMap(), null, null, null,
new ReferencingFactoryContainer(null, null, null, null, null, mtFactory), null);
}
/**
* Creates a parser using the specified set of symbols and factories.
*
* @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 factories the factories to use for creating math transforms and geodetic objects.
* @param errorLocale the locale for error messages (not for parsing), or {@code null} for the system default.
*/
MathTransformParser(final Symbols symbols, final Map<String,Element> fragments,
final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
final ReferencingFactoryContainer factories, final Locale errorLocale)
{
super(symbols, fragments, numberFormat, dateFormat, unitFormat, errorLocale);
this.factories = factories;
ensureNonNull("factories", factories);
}
/**
* 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.operation.transform.DefaultMathTransformFactory";
}
/**
* 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, or {@code null} if the element is not recognized.
* @throws ParseException if the element can not be parsed.
*/
@Override
Object parseObject(final Element element) throws ParseException {
return parseMathTransform(element, true);
}
/**
* Parses the next {@code MathTransform} in the specified <cite>Well Know Text</cite> (WKT) tree.
*
* @param element the parent element.
* @param mandatory {@code true} if a math transform must be present, or {@code false} if optional.
* @return the next element as a {@code MathTransform} object, or {@code null}.
* @throws ParseException if the next element can not be parsed.
*/
final MathTransform parseMathTransform(final Element element, final boolean mandatory) throws ParseException {
lastMethod = null;
classification = null;
MathTransform tr;
if ((tr = parseParamMT (element)) == null &&
(tr = parseConcatMT (element)) == null &&
(tr = parseInverseMT (element)) == null &&
(tr = parsePassThroughMT (element)) == null)
{
if (mandatory) {
throw element.missingOrUnknownComponent(WKTKeywords.Param_MT);
}
}
return tr;
}
/**
* Parses the {@code ID["authority", "code"]} element inside a {@code UNIT} element.
* If such element is found, the authority is {@code "EPSG"} and the code is one of
* the codes known to the {@link Units#valueOfEPSG(int)}, then that unit is returned.
* Otherwise this method returns null.
*
* <div class="note"><b>Note:</b>
* this method is a slight departure of ISO 19162, which said <cite>"Should any attributes or values given
* in the cited identifier be in conflict with attributes or values given explicitly in the WKT description,
* the WKT values shall prevail."</cite> But some units can hardly be expressed by the {@code UNIT} element,
* because the later can contain only a conversion factor. For example sexagesimal units (EPSG:9108, 9110
* and 9111) can hardly be expressed in an other way than by their EPSG code. Thankfully, identifiers in
* {@code UNIT} elements are rare, so risk of conflicts should be low.</div>
*
* @param parent the parent {@code "UNIT"} element.
* @return the unit from the identifier code, or {@code null} if none.
* @throws ParseException if the {@code "ID"} can not be parsed.
*/
final Unit<?> parseUnitID(final Element parent) throws ParseException {
final Element element = parent.pullElement(OPTIONAL, ID_KEYWORDS);
if (element != null) {
final String codeSpace = element.pullString("codeSpace");
final Object code = element.pullObject("code"); // Accepts Integer as well as String.
element.close(ignoredElements);
if (Constants.EPSG.equalsIgnoreCase(codeSpace)) try {
final int n;
if (Numbers.isInteger(code.getClass())) {
n = ((Number) code).intValue();
} else {
n = Integer.parseInt(code.toString());
}
return Units.valueOfEPSG(n);
} catch (NumberFormatException e) {
warning(parent, element, null, e);
}
}
return null;
}
/**
* Parses an optional {@code "UNIT"} element of unknown dimension.
* This method tries to infer the quantity dimension from the unit keyword.
*
* @param parent the parent element.
* @return the {@code "UNIT"} element, or {@code null} if none.
* @throws ParseException if the {@code "UNIT"} can not be parsed.
*
* @see GeodeticObjectParser#parseScaledUnit(Element, String, Unit)
*/
final Unit<?> parseUnit(final Element parent) throws ParseException {
final Element element = parent.pullElement(OPTIONAL, UNIT_KEYWORDS);
if (element == null) {
return null;
}
final String name = element.pullString("name");
double factor = element.pullDouble("factor");
final int index = element.getKeywordIndex() - 1;
final Unit<?> unit = parseUnitID(element);
element.close(ignoredElements);
if (unit != null) {
return unit;
}
/*
* Conversion factor can be applied only if the base dimension (angle, linear, scale, etc.) is known.
* However before to apply that factor, we may need to fix rounding errors found in some WKT strings.
* In particular, the conversion factor for degrees is sometime written as 0.01745329252 instead of
* 0.017453292519943295.
*/
if (index >= 0 && index < BASE_UNITS.length) {
if (index < CONVERSION_FACTORS.length) {
factor = completeUnitFactor(CONVERSION_FACTORS[index], factor);
}
return BASE_UNITS[index].multiply(factor);
}
// If we can not infer the base type, we have to rely on the name.
try {
return parseUnit(name);
} catch (ParserException e) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.UnknownUnit_1,
new Object[] {name}, element.offset).initCause(e);
}
}
/**
* If the unit conversion factor specified in the Well Known Text is missing some fraction digits,
* tries to complete them. The main use case is to replace 0.01745329252 by 0.017453292519943295
* in degree units.
*
* @param predefined some known conversion factors, in ascending order.
* @param factor the conversion factor specified in the Well Known Text element.
* @return the conversion factor to use.
*/
private static double completeUnitFactor(final double[] predefined, final double factor) {
int i = Arrays.binarySearch(predefined, factor);
if (i < 0) {
i = Math.max(~i, 1);
double accurate = predefined[i-1];
if (i < predefined.length) {
double next = predefined[i];
if (next - factor < factor - accurate) {
accurate = next;
}
}
if (DecimalFunctions.equalsIgnoreMissingFractionDigits(accurate, factor)) {
return accurate;
}
}
return factor;
}
/**
* If the unit conversion factor specified in the Well Known Text is missing some fraction digits,
* tries to complete them. The main use case is to replace 0.01745329252 by 0.017453292519943295
* in degree units.
*
* @param baseUnit the base unit for which to complete the conversion factor.
* @param factor the conversion factor specified in the Well Known Text element.
* @return the conversion factor to use.
*/
static double completeUnitFactor(final Unit<?> baseUnit, final double factor) {
for (int i=CONVERSION_FACTORS.length; --i>=0;) {
if (BASE_UNITS[i] == baseUnit) {
return completeUnitFactor(CONVERSION_FACTORS[i], factor);
}
}
return factor;
}
/**
* Parses a sequence of {@code "PARAMETER"} elements.
*
* @param element the parent element containing the parameters to parse.
* @param parameters the group where to store the parameter values.
* @param defaultUnit the default unit (for arbitrary quantity, including angular), or {@code null}.
* @param defaultAngularUnit the default angular unit, or {@code null} if none. This is determined by the context,
* especially when {@link GeodeticObjectParser} parses a {@code ProjectedCRS} element.
* @throws ParseException if the {@code "PARAMETER"} element can not be parsed.
*/
final void parseParameters(final Element element, final ParameterValueGroup parameters,
final Unit<?> defaultUnit, final Unit<Angle> defaultAngularUnit) throws ParseException
{
final Unit<?> defaultSI = (defaultUnit != null) ? defaultUnit.getSystemUnit() : null;
Element param = element;
try {
while ((param = element.pullElement(OPTIONAL, WKTKeywords.Parameter)) != null) {
final String name = param.pullString("name");
Unit<?> unit = parseUnit(param);
param.pullElement(OPTIONAL, ID_KEYWORDS);
/*
* DEPARTURE FROM ISO 19162: the specification recommends that we use the identifier instead
* than the parameter name. However we do not yet have a "get parameter by ID" in Apache SIS
* or in GeoAPI interfaces. This was not considered necessary since SIS is lenient (hopefully
* without introducing ambiguity) regarding parameter names, but we may revisit in a future
* version if it become no longer the case. See https://issues.apache.org/jira/browse/SIS-210
*/
final ParameterValue<?> parameter = parameters.parameter(name);
final ParameterDescriptor<?> descriptor = parameter.getDescriptor();
final Class<?> valueClass = descriptor.getValueClass();
final boolean isNumeric = Number.class.isAssignableFrom(valueClass);
if (isNumeric && unit == null) {
unit = descriptor.getUnit();
if (unit != null) {
final Unit<?> si = unit.getSystemUnit();
if (si.equals(defaultSI)) {
unit = defaultUnit;
} else if (si.equals(Units.RADIAN)) {
unit = defaultAngularUnit;
}
}
}
if (unit != null) {
parameter.setValue(param.pullDouble("doubleValue"), unit);
} else if (isNumeric) {
if (Numbers.isInteger(valueClass)) {
parameter.setValue(param.pullInteger("intValue"));
} else {
parameter.setValue(param.pullDouble("doubleValue"));
}
} else if (valueClass == Boolean.class) {
parameter.setValue(param.pullBoolean("booleanValue"));
} else {
parameter.setValue(param.pullString("stringValue"));
}
param.close(ignoredElements);
}
} catch (ParameterNotFoundException e) {
throw new UnparsableObjectException(errorLocale, Errors.Keys.UnexpectedParameter_1,
new String[] {e.getParameterName()}, param.offset).initCause(e);
} catch (InvalidParameterValueException e) {
throw (ParseException) new ParseException(e.getLocalizedMessage(), param.offset).initCause(e);
}
}
/**
* Parses a {@code "PARAM_MT"} element. This element has the following pattern:
*
* {@preformat text
* PARAM_MT["<classification-name>" {,<parameter>}* ]
* }
*
* @param parent the parent element.
* @return the {@code "PARAM_MT"} element as an {@link MathTransform} object.
* @throws ParseException if the {@code "PARAM_MT"} element can not be parsed.
*/
private MathTransform parseParamMT(final Element parent) throws ParseException {
final Element element = parent.pullElement(FIRST, WKTKeywords.Param_MT);
if (element == null) {
return null;
}
classification = element.pullString("classification");
final MathTransformFactory mtFactory = factories.getMathTransformFactory();
final ParameterValueGroup parameters;
try {
parameters = mtFactory.getDefaultParameters(classification);
} catch (NoSuchIdentifierException exception) {
throw element.parseFailed(exception);
}
/*
* Scan over all PARAMETER["name", value] elements and
* set the corresponding parameter in the parameter group.
*/
parseParameters(element, parameters, null, null);
element.close(ignoredElements);
/*
* We now have all information for constructing the math transform.
*/
final MathTransform transform;
try {
transform = mtFactory.createParameterizedTransform(parameters);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
lastMethod = mtFactory.getLastMethodUsed();
return transform;
}
/**
* Parses an {@code "INVERSE_MT"} element. This element has the following pattern:
*
* {@preformat text
* INVERSE_MT[<math transform>]
* }
*
* @param parent the parent element.
* @return the {@code "INVERSE_MT"} element as an {@link MathTransform} object.
* @throws ParseException if the {@code "INVERSE_MT"} element can not be parsed.
*/
private MathTransform parseInverseMT(final Element parent) throws ParseException {
final Element element = parent.pullElement(FIRST, WKTKeywords.Inverse_MT);
if (element == null) {
return null;
}
MathTransform transform = parseMathTransform(element, true);
try {
transform = transform.inverse();
} catch (NoninvertibleTransformException exception) {
throw element.parseFailed(exception);
}
element.close(ignoredElements);
return transform;
}
/**
* Parses a {@code "PASSTHROUGH_MT"} element. This element has the following pattern:
*
* {@preformat text
* PASSTHROUGH_MT[<integer>, <math transform>]
* }
*
* @param parent the parent element.
* @return the {@code "PASSTHROUGH_MT"} element as an {@link MathTransform} object.
* @throws ParseException if the {@code "PASSTHROUGH_MT"} element can not be parsed.
*/
private MathTransform parsePassThroughMT(final Element parent) throws ParseException {
final Element element = parent.pullElement(FIRST, WKTKeywords.PassThrough_MT);
if (element == null) {
return null;
}
final int firstAffectedCoordinate = parent.pullInteger("firstAffectedCoordinate");
final MathTransform transform = parseMathTransform(element, true);
final MathTransformFactory mtFactory = factories.getMathTransformFactory();
element.close(ignoredElements);
try {
return mtFactory.createPassThroughTransform(firstAffectedCoordinate, transform, 0);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
/**
* Parses a {@code "CONCAT_MT"} element. This element has the following pattern:
*
* {@preformat text
* CONCAT_MT[<math transform> {,<math transform>}*]
* }
*
* @param parent the parent element.
* @return the {@code "CONCAT_MT"} element as an {@link MathTransform} object.
* @throws ParseException if the {@code "CONCAT_MT"} element can not be parsed.
*/
private MathTransform parseConcatMT(final Element parent) throws ParseException {
final Element element = parent.pullElement(FIRST, WKTKeywords.Concat_MT);
if (element == null) {
return null;
}
MathTransform transform = parseMathTransform(element, true);
MathTransform optionalTransform;
final MathTransformFactory mtFactory = factories.getMathTransformFactory();
while ((optionalTransform = parseMathTransform(element, false)) != null) {
try {
transform = mtFactory.createConcatenatedTransform(transform, optionalTransform);
} catch (FactoryException exception) {
throw element.parseFailed(exception);
}
}
element.close(ignoredElements);
return transform;
}
/**
* Returns the operation method for the last math transform parsed. This is used by
* {@link GeodeticObjectParser} in order to built {@link org.opengis.referencing.crs.DerivedCRS}.
*/
final OperationMethod getOperationMethod() {
if (lastMethod == null) {
/*
* Safety in case some MathTransformFactory implementations do not support
* getLastMethod(). Performs a slower and less robust check as a fallback.
*/
if (classification != null) {
final MathTransformFactory mtFactory = factories.getMathTransformFactory();
lastMethod = CoordinateOperations.getOperationMethod(
mtFactory.getAvailableMethods(SingleOperation.class), classification);
}
}
return lastMethod;
}
}