| /* |
| * 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; |
| } |
| } |