This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 062a8de71d263f94514009c167775fa11719162b Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Sep 6 16:53:42 2025 +0200 Add the support of `AXISMINVALUE`, `AXISMAXVALUE` and `RANGEMEANING` from ISO 19162:2019. --- .../main/org/apache/sis/io/wkt/AbstractParser.java | 73 ++++++++++++++ .../main/org/apache/sis/io/wkt/Convention.java | 4 +- .../main/org/apache/sis/io/wkt/Element.java | 2 +- .../main/org/apache/sis/io/wkt/Formatter.java | 2 +- .../apache/sis/io/wkt/GeodeticObjectParser.java | 55 ++++------- .../org/apache/sis/io/wkt/MathTransformParser.java | 25 +++-- .../main/org/apache/sis/io/wkt/Transliterator.java | 20 ++-- .../cs/DefaultCoordinateSystemAxis.java | 105 ++++++++++++++++----- .../apache/sis/referencing/privy/WKTKeywords.java | 16 ++-- .../sis/io/wkt/GeodeticObjectParserTest.java | 18 ++++ .../test/org/apache/sis/io/wkt/WKTFormatTest.java | 1 + .../referencing/crs/DefaultGeographicCRSTest.java | 4 +- .../referencing/crs/DefaultProjectedCRSTest.java | 4 +- .../cs/DefaultCoordinateSystemAxisTest.java | 41 ++++---- 14 files changed, 261 insertions(+), 109 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java index 44a49c5e9e..d6abe3c310 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java @@ -420,6 +420,79 @@ abstract class AbstractParser implements Parser { return unitFormat.parse(text); } + /** + * Pulls an optional element which contains only a floating-point value. + * + * @param parent the element from which to pull a child element. + * @param key name of the element to pull. + * @return the value, or {@code null} if none. + * @throws ParseException if an element cannot be parsed. + */ + final Double pullElementAsDouble(final Element parent, final String key) throws ParseException { + Element element = parent.pullElement(OPTIONAL, key); + if (element != null) { + double value = element.pullDouble(key); + element.close(ignoredElements); + return value; + } + return null; + } + + /** + * Pulls an optional element which contains only an integer value. + * + * @param parent the element from which to pull a child element. + * @param key name of the element to pull. + * @return the value, or {@code null} if none. + * @throws ParseException if an element cannot be parsed. + */ + final Integer pullElementAsInteger(final Element parent, final String key) throws ParseException { + Element element = parent.pullElement(OPTIONAL, key); + if (element != null) { + int value = element.pullInteger(key); + element.close(ignoredElements); + return value; + } + return null; + } + + /** + * Pulls an optional element which contains only a string value. + * + * @param parent the element from which to pull a child element. + * @param key name of the element to pull. + * @return the value, or {@code null} if none. + * @throws ParseException if an element cannot be parsed. + */ + final String pullElementAsString(final Element parent, final String key) throws ParseException { + Element element = parent.pullElement(OPTIONAL, key); + if (element != null) { + String value = element.pullString(key); + element.close(ignoredElements); + return value; + } + return null; + } + + /** + * Pulls an optional element which contains only an enumeration value. + * + * @param parent the element from which to pull a child element. + * @param key name of the element to pull. + * @return the value, or {@code null} if none. + * @throws ParseException if an element cannot be parsed. + */ + final String pullElementAsEnum(final Element parent, final String key) throws ParseException { + Element element = parent.pullElement(OPTIONAL, key); + if (element != null) { + Element value = element.pullVoidElement(key); + value .close(ignoredElements); + element.close(ignoredElements); + return value.keyword; + } + return null; + } + /** * Reports a non-fatal warning that occurred while parsing a WKT. * diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java index 30b94cdca9..7fc4c24e13 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Convention.java @@ -77,7 +77,9 @@ public enum Convention { * <li>By {@linkplain KeywordStyle#DEFAULT default}, this convention uses the keywords that are the closest matches * to the Java interface names. For example, {@code "GeodeticCRS"} is preferred to {@code "GeodCRS"}.</li> * <li>The {@code PrimeMeridian} element is omitted if the meridian is Greenwich.</li> - * <li>The {@code Axis} element omits the {@code Order} sub-element.</li> + * <li>The {@code Axis} element omits always the {@code Order} sub-element.</li> + * <li>The {@code Axis} element omits the {@code AxisMinValue}, {@code AxisMinValue} and {@code RangeMeaning} + * sub-elements if their values are the standard values for latitude or longitude axes.</li> * <li>The {@code Unit} elements are less verbose:<ul> * <li>{@code Ellipsoid} and {@code VerticalExtent} elements omit the {@code LengthUnit} sub-element * if that unit is {@link org.apache.sis.measure.Units#METRE}.</li> diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java index 87d0938106..e34dddbdd8 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java @@ -713,7 +713,7 @@ final class Element { /** * Removes and returns the next {@link Element} with no bracket. - * The key is used only for only for formatting an error message. + * The key is used only for formatting an error message. * * @param key the parameter name. Used only for formatting an error message. * @return the next {@link Element} among the children, with no bracket. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java index 4a8ec2309f..8aa6c12db6 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java @@ -1622,7 +1622,7 @@ public class Formatter implements Localized { */ final boolean appendValue(final Object value) { if (value instanceof Number) { - final Number number = (Number) value; + final var number = (Number) value; if (Numbers.isInteger(number.getClass())) { append(number.longValue()); } else { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java index 8e9e9d85fa..d64fc1d548 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java @@ -57,6 +57,7 @@ import org.apache.sis.referencing.ImmutableIdentifier; import org.apache.sis.referencing.DefaultObjectDomain; import org.apache.sis.referencing.cs.AbstractCS; import org.apache.sis.referencing.cs.CoordinateSystems; +import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis; import org.apache.sis.referencing.crs.DefaultDerivedCRS; import org.apache.sis.referencing.datum.BursaWolfParameters; import org.apache.sis.referencing.datum.DefaultGeodeticDatum; @@ -474,7 +475,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo */ final var domains = new ArrayList<ObjectDomain>(); while ((element = parent.pullElement(OPTIONAL, WKTKeywords.Usage)) != null) { - final String scope = parseScope(element); + final String scope = pullElementAsString(element, WKTKeywords.Scope); final DefaultExtent extent = parseExtent(element); if (scope != null || extent != null) { domains.add(new DefaultObjectDomain(Types.toInternationalString(scope), extent)); @@ -486,7 +487,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo * Legacy (ISO 19162:2015) way to declare scope and extent: * directly inside de CRS element, without USAGE wrapper. */ - final String scope = parseScope(parent); + final String scope = pullElementAsString(parent, WKTKeywords.Scope); if (scope != null) { properties.put(ObjectDomain.SCOPE_KEY, scope); } @@ -526,28 +527,6 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo return properties; } - /** - * Parses the {@code SCOPE} element. - * This element has the following pattern: - * - * {@snippet lang="wkt" : - * SCOPE["Large scale topographic mapping and cadastre."] - * } - * - * @param parent the parent element. - * @return the scope, or {@code null} if none. - * @throws ParseException if an element cannot be parsed. - */ - private String parseScope(final Element parent) throws ParseException { - final Element element = parent.pullElement(OPTIONAL, WKTKeywords.Scope); - if (element != null) { - final String scope = element.pullString("scope"); - element.close(ignoredElements); - return scope; - } - return null; - } - /** * Parses the {@code AREA}, {@code BBOX}, {@code VERTICALEXTENT} and {@code TIMEEXTENT} elements if present. * These elements were directly inside the <abbr>CRS</abbr> element in <abbr>ISO</abbr> 19162:2015, but became @@ -1127,24 +1106,30 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo 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. + * At this point, the mandatory properties are known. The next element is specified by ISO 19162 + * but not stored in Apache SIS representation of axis: ORDER[n], which specifies the axis ordering. + * If present, we will store that value for processing by the `parseCoordinateSystem(…)` method. + */ + final Integer order = pullElementAsInteger(element, WKTKeywords.Order); + final Double minimum = pullElementAsDouble (element, WKTKeywords.AxisMinValue); + final Double maximum = pullElementAsDouble (element, WKTKeywords.AxisMaxValue); + final String meaning = pullElementAsEnum (element, WKTKeywords.RangeMeaning); + final Map<String, Object> properties = parseMetadataAndClose(element, name, null); + properties.put(DefaultCoordinateSystemAxis.MINIMUM_VALUE_KEY, minimum); + properties.put(DefaultCoordinateSystemAxis.MAXIMUM_VALUE_KEY, maximum); + properties.put(DefaultCoordinateSystemAxis.RANGE_MEANING_KEY, + Types.forCodeName(RangeMeaning.class, meaning, RangeMeaning::valueOf)); + /* + * At this point, all mandatory and optional properties are known. */ - 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); + axis = csFactory.createCoordinateSystemAxis(properties, 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. + if (axisOrder.put(axis, order) != 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); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java index 59fe947a89..d2712e0309 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java @@ -68,7 +68,8 @@ class MathTransformParser extends AbstractParser { 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 as "Unit". + WKTKeywords.TemporalQuantity, // Alternative keyword for "TimeUnit". + WKTKeywords.ParametricUnit // Ignored for the same reason as "Unit". }; /** @@ -76,7 +77,7 @@ class MathTransformParser extends AbstractParser { * 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 + Units.METRE, Units.RADIAN, Units.UNITY, Units.SECOND, Units.SECOND }; /** @@ -254,10 +255,22 @@ class MathTransformParser extends AbstractParser { if (element == null) { return null; } - final String name = element.pullString("name"); - double factor = element.pullDouble("factor"); final int index = element.getKeywordIndex() - 1; + final String name = element.pullString("name"); final Unit<?> unit = parseUnitID(element); + final Unit<?> base = (index >= 0 && index < BASE_UNITS.length) ? BASE_UNITS[index] : null; + /* + * The conversion factor form base unit is mandatory, except for temporal units + * because the conversion may not be exact (because of variable duration of day, + * month or year). Note however that Apache SIS 1.5 will fallback on constant + * factors anyway, therefore the problem described by ISO is not really solved. + */ + double factor; + if (base == Units.SECOND && element.peekValue() == null) { + factor = Double.NaN; + } else { + factor = element.pullDouble("factor"); + } element.close(ignoredElements); if (unit != null) { return unit; @@ -268,11 +281,11 @@ class MathTransformParser extends AbstractParser { * In particular, the conversion factor for degrees is sometimes written as 0.01745329252 instead of * 0.017453292519943295. */ - if (index >= 0 && index < BASE_UNITS.length) { + if (base != null && !Double.isNaN(factor)) { if (index < CONVERSION_FACTORS.length) { factor = completeUnitFactor(CONVERSION_FACTORS[index], factor); } - return BASE_UNITS[index].multiply(factor); + return base.multiply(factor); } // If we cannot infer the base type, we have to rely on the name. try { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java index 6d47f0ba38..f3359384bb 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Transliterator.java @@ -29,6 +29,7 @@ import org.apache.sis.referencing.privy.AxisDirections; import org.apache.sis.referencing.privy.WKTKeywords; import org.apache.sis.util.CharSequences; import org.apache.sis.util.Characters; +import org.apache.sis.util.OptionalCandidate; /** @@ -181,17 +182,15 @@ public abstract class Transliterator implements Serializable { } /** - * Returns the axis name to format in WKT, or {@code null} if none. This method performs the mapping - * between the names of axes in memory (designated by <q>long axis names</q> in this class) - * and the names to format in the WKT (designated by <q>short axis names</q>). + * Returns the axis name to format in <abbr>WKT</abbr>, or {@code null} if none. + * This method performs the mapping between the names of axes in memory (designated by <dfn>long axis names</dfn> + * in this class) and the names to format in the <abbr>WKT</abbr> (designated by <dfn>short axis names</dfn>). + * The long axis names are defined by <abbr>ISO</abbr> 19111 — <cite>referencing by coordinates</cite> while + * the short axis names are defined by <abbr>ISO</abbr> 19162 — <cite>Well-known text representation + * of coordinate reference systems</cite>. * - * <div class="note"><b>Note:</b> - * the <q>long axis names</q> are defined by ISO 19111 — <cite>referencing by coordinates</cite> - * while the <q>short axis names</q> are defined by ISO 19162 — <cite>Well-known text representation - * of coordinate reference systems</cite>.</div> - * - * This method can return {@code null} if the name should be omitted. - * ISO 19162 recommends to omit the axis name when it is already given through the mandatory axis direction. + * <p>This method returns {@code null} if the name should be omitted. <abbr>ISO</abbr> 19162 recommends + * to omit the axis name when it is already given through the mandatory axis direction.</p> * * <p>The default implementation performs at least the following replacements:</p> * <ul> @@ -208,6 +207,7 @@ public abstract class Transliterator implements Serializable { * * @see org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis#formatTo(Formatter) */ + @OptionalCandidate public String toShortAxisName(final CoordinateSystem cs, final AxisDirection direction, final String name) { if (name.equalsIgnoreCase(AxisNames.GEODETIC_LATITUDE) || // ISO 19162:2015 §7.5.3(ii) name.equalsIgnoreCase(AxisNames.PLANETODETIC_LATITUDE)) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java index 1eda1dc32e..e332509b3c 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxis.java @@ -48,8 +48,10 @@ import org.apache.sis.referencing.privy.AxisDirections; import org.apache.sis.measure.Longitude; import org.apache.sis.measure.Latitude; import org.apache.sis.measure.Units; +import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Utilities; import org.apache.sis.util.ComparisonMode; +import org.apache.sis.util.collection.Containers; import org.apache.sis.util.resources.Errors; import org.apache.sis.xml.bind.Context; import org.apache.sis.io.wkt.Formatter; @@ -57,8 +59,6 @@ import org.apache.sis.io.wkt.Convention; import org.apache.sis.io.wkt.ElementKind; import org.apache.sis.io.wkt.Transliterator; import org.apache.sis.io.wkt.FormattableObject; -import static org.apache.sis.util.ArgumentChecks.*; -import static org.apache.sis.util.collection.Containers.property; /* @@ -296,12 +296,12 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem this.abbreviation = abbreviation; this.direction = direction; this.unit = unit; - ensureNonEmpty("abbreviation", abbreviation); - ensureNonNull ("direction", direction); - ensureNonNull ("unit", unit); - Number minimum = property(properties, MINIMUM_VALUE_KEY, Number.class); - Number maximum = property(properties, MAXIMUM_VALUE_KEY, Number.class); - RangeMeaning rm = property(properties, RANGE_MEANING_KEY, RangeMeaning.class); + ArgumentChecks.ensureNonEmpty("abbreviation", abbreviation); + ArgumentChecks.ensureNonNull ("direction", direction); + ArgumentChecks.ensureNonNull ("unit", unit); + Number minimum = Containers.property(properties, MINIMUM_VALUE_KEY, Number.class); + Number maximum = Containers.property(properties, MAXIMUM_VALUE_KEY, Number.class); + RangeMeaning rm = Containers.property(properties, RANGE_MEANING_KEY, RangeMeaning.class); if (minimum == null && maximum == null && rm == null) { double min = Double.NEGATIVE_INFINITY; double max = Double.POSITIVE_INFINITY; @@ -333,7 +333,7 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem Errors.Keys.IllegalRange_2, minimumValue, maximumValue)); } if ((minimumValue != NEGATIVE_INFINITY) || (maximumValue != POSITIVE_INFINITY)) { - ensureNonNull(RANGE_MEANING_KEY, rm); + ArgumentChecks.ensureNonNull(RANGE_MEANING_KEY, rm); } else { rm = null; } @@ -781,7 +781,7 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem */ if (!isWKT1) { if (cs != null && !convention.isSimplified()) { - final Order order = Order.create(cs, this); + final Child order = Child.order(cs, this); if (order != null) { formatter.append(order); } else { @@ -791,19 +791,52 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem if (!formatter.hasContextualUnit(1)) { formatter.append(getUnit()); } + if (convention.supports(Convention.WKT2_2019)) { + boolean standard = false; + if (convention.isSimplified()) { + dir = AxisDirections.absolute(dir); + if (Units.DEGREE.equals(unit)) { + if (dir.equals(AxisDirection.NORTH)) { + standard = (minimumValue == Latitude.MIN_VALUE) + && (maximumValue == Latitude.MAX_VALUE) + && (rangeMeaning == RangeMeaning.EXACT); + } else if (dir.equals(AxisDirection.EAST)) { + standard = (minimumValue == Longitude.MIN_VALUE) + && (maximumValue == Longitude.MAX_VALUE) + && (rangeMeaning == RangeMeaning.WRAPAROUND); + } + } else if (Units.isLinear(unit) && cs instanceof SphericalCS) { + if (dir.equals(AxisDirection.UP)) { + standard = (minimumValue == 0) + && (maximumValue == Double.POSITIVE_INFINITY) + && (rangeMeaning == RangeMeaning.EXACT); + } + } + } + if (!standard) { + final Child min = Child.range(getMinimumValue(), false); + final Child max = Child.range(getMaximumValue(), true); + if (min != null || max != null) { + formatter.append(min); + formatter.append(max); + formatter.append(Child.range(getRangeMeaning())); + } + } + } } return WKTKeywords.Axis; } /** - * The {@code ORDER[…]} element to be formatted inside {@code AXIS[…]} element. - * This is an element of WKT 2 only. + * The {@code ORDER[…]}, {@code AXISMINVALUE}, {@code AXISMAXVALUE} or {@code RANGEMEANING} element + * to be formatted inside an {@code AXIS[…]} element. They are elements of <abbr>WKT</abbr> 2 only. */ - private static final class Order extends FormattableObject { - /** - * The sequence number to format inside the {@code ORDER[…]} element. - */ - private final int index; + private static final class Child extends FormattableObject { + /** The <abbr>WKT</abbr> keyword of this element. */ + private final String keyword; + + /** The numerical or textual value to be written inside this child element. */ + private final Object value; /** * Creates a new {@code ORDER[…]} element for the given axis in the given coordinate system. @@ -817,13 +850,13 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem * * @se <a href="https://issues.apache.org/jira/browse/SIS-163">SIS-163</a> */ - static Order create(final CoordinateSystem cs, final DefaultCoordinateSystemAxis axis) { - Order order = null; + static Child order(final CoordinateSystem cs, final DefaultCoordinateSystemAxis axis) { + Child order = null; final int dimension = cs.getDimension(); for (int i=0; i<dimension;) { if (cs.getAxis(i++) == axis) { if (order == null) { - order = new Order(i); + order = new Child(WKTKeywords.Order, i); } else { return null; } @@ -833,19 +866,39 @@ public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implem } /** - * Creates new {@code ORDER[…]} element for the given sequential number. + * Creates a new {@code AXISMINVALUE} or {@code AXISMAXVALUE} element for the given value. + * If the value is not finite, then this method returns {@code null}. + */ + static Child range(final double value, final boolean isMax) { + if (Double.isFinite(value)) { + return new Child(isMax ? WKTKeywords.AxisMaxValue : WKTKeywords.AxisMinValue, value); + } + return null; + } + + /** + * Creates a new {@code RANGEMEANING} element for the given value. + * If the value is null, then this method returns {@code null}. + */ + static Child range(final RangeMeaning value) { + return (value != null) ? new Child(WKTKeywords.RangeMeaning, value) : null; + } + + /** + * Creates new child element with the given value. */ - private Order(final int index) { - this.index = index; + private Child(final String keyword, final Object value) { + this.keyword = keyword; + this.value = value; } /** - * Formats the {@code ORDER[…]} element. + * Formats the {@code ORDER[…]}, {@code AXISMINVALUE}, {@code AXISMAXVALUE} or {@code RANGEMEANING} element. */ @Override protected String formatTo(final Formatter formatter) { - formatter.append(index); - return WKTKeywords.Order; + formatter.appendAny(value); + return keyword; } } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java index 5e95187704..80ad11133c 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTKeywords.java @@ -65,12 +65,13 @@ public final class WKTKeywords extends Static { * Related to unit of measurements. */ public static final String - Unit = "Unit", - LengthUnit = "LengthUnit", - AngleUnit = "AngleUnit", - ScaleUnit = "ScaleUnit", - TimeUnit = "TimeUnit", - ParametricUnit = "ParametricUnit"; + Unit = "Unit", + LengthUnit = "LengthUnit", + AngleUnit = "AngleUnit", + ScaleUnit = "ScaleUnit", + TimeUnit = "TimeUnit", + TemporalQuantity = "TemporalQuantity", + ParametricUnit = "ParametricUnit"; /** * Related to {@link org.apache.sis.referencing.cs.AbstractCS} @@ -79,6 +80,9 @@ public final class WKTKeywords extends Static { public static final String CS = "CS", Axis = "Axis", + AxisMinValue = "AxisMinValue", + AxisMaxValue = "AxisMaxValue", + RangeMeaning = "RangeMeaning", Order = "Order", Meridian = "Meridian", PrimeMeridian = "PrimeMeridian", diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java index 5ad05410be..074b3c4774 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java @@ -34,6 +34,8 @@ import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; +import org.apache.sis.measure.Latitude; +import org.apache.sis.measure.Longitude; import org.apache.sis.metadata.privy.AxisNames; import org.apache.sis.referencing.privy.ReferencingFactoryContainer; import org.apache.sis.referencing.cs.CoordinateSystems; @@ -184,12 +186,18 @@ public final class GeodeticObjectParserTest extends EPSGDependentTestCase { assertEquals("φ", axis.getAbbreviation(), "abbreviation"); assertEquals(AxisDirection.NORTH, axis.getDirection(), "direction"); assertEquals(Units.DEGREE, axis.getUnit(), "unit"); + assertEquals(Latitude.MIN_VALUE, axis.getMinimumValue()); + assertEquals(Latitude.MAX_VALUE, axis.getMaximumValue()); + assertEquals(RangeMeaning.EXACT, axis.getRangeMeaning()); axis = parse(CoordinateSystemAxis.class, "AXIS[“longitude”,EAST,order[2],UNIT[“degree”,0.0174532925199433]]"); assertEquals("Longitude", axis.getName().getCode(), "name"); assertEquals("λ", axis.getAbbreviation(), "abbreviation"); assertEquals(AxisDirection.EAST, axis.getDirection(), "direction"); assertEquals(Units.DEGREE, axis.getUnit(), "unit"); + assertEquals(Longitude.MIN_VALUE, axis.getMinimumValue()); + assertEquals(Longitude.MAX_VALUE, axis.getMaximumValue()); + assertEquals(RangeMeaning.WRAPAROUND, axis.getRangeMeaning()); axis = parse(CoordinateSystemAxis.class, "AXIS[“ellipsoidal height (h)”,up,ORDER[3],LengthUnit[“kilometre”,1000]]"); assertEquals("Ellipsoidal height", axis.getName().getCode(), "name"); @@ -208,6 +216,16 @@ public final class GeodeticObjectParserTest extends EPSGDependentTestCase { assertEquals("X", axis.getAbbreviation(), "abbreviation"); assertEquals(CoordinateSystems.directionAlongMeridian(AxisDirection.SOUTH, 90), axis.getDirection(), "direction"); assertEquals(Units.METRE, axis.getUnit(), "unit"); + + axis = parse(CoordinateSystemAxis.class, "AXIS[“longitude”,EAST,order[2],UNIT[“degree”,0.0174532925199433]," + + "AxisMinValue[0],AxisMaxValue[360],RangeMeaning[wraparound]]"); + assertEquals("Longitude", axis.getName().getCode(), "name"); + assertEquals("λ", axis.getAbbreviation(), "abbreviation"); + assertEquals(AxisDirection.EAST, axis.getDirection(), "direction"); + assertEquals(Units.DEGREE, axis.getUnit(), "unit"); + assertEquals(RangeMeaning.WRAPAROUND, axis.getRangeMeaning()); + assertEquals( 0, axis.getMinimumValue()); + assertEquals(360, axis.getMaximumValue()); } /** diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java index 1a556fdeb6..45c21869bf 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/WKTFormatTest.java @@ -458,6 +458,7 @@ public final class WKTFormatTest extends EPSGDependentTestCase { @Test public void testFragments() throws ParseException { format = new WKTFormat(); + format.setConvention(Convention.WKT2_2015); format.addFragment("deg", "UNIT[“degree”, 0.0174532925199433]"); format.addFragment("Bessel", "SPHEROID[“Bessel 1841”, 6377397.155, 299.1528128, AUTHORITY[“EPSG”,“7004”]]"); format.addFragment("Tokyo", "DATUM[“Tokyo”, $Bessel]"); diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java index 931da16470..5d52def9d4 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultGeographicCRSTest.java @@ -247,8 +247,8 @@ public final class DefaultGeographicCRSTest extends TestCase { " Ellipsoid[“NTF”, 6378249.2, 293.4660212936269]],\n" + " PrimeMeridian[“Paris”, 2.5969213, Unit[“grad”, 0.015707963267948967]],\n" + " CS[ellipsoidal, 2],\n" + - " Axis[“Longitude (L)”, east],\n" + // See method javadoc. - " Axis[“Latitude (B)”, north],\n" + + " Axis[“Longitude (L)”, east, AxisMinValue[-200.0], AxisMaxValue[200.0], RangeMeaning[wraparound]],\n" + + " Axis[“Latitude (B)”, north, AxisMinValue[-100.0], AxisMaxValue[100.0], RangeMeaning[exact]],\n" + " Unit[“grad”, 0.015707963267948967]]", HardCodedCRS.NTF); } diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java index faf4f9e82a..9392c3a5fc 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/DefaultProjectedCRSTest.java @@ -274,8 +274,8 @@ public final class DefaultProjectedCRSTest extends TestCase.WithLogs { " Id[“EPSG”, 6807]],\n" + " PrimeMeridian[“Paris”, 2.5969213, Id[“EPSG”, 8903]],\n" + " CS[ellipsoidal, 2],\n" + - " Axis[“Longitude (λ)”, east],\n" + - " Axis[“Latitude (φ)”, north],\n" + + " Axis[“Longitude (λ)”, east, AxisMinValue[-200.0], AxisMaxValue[200.0], RangeMeaning[wraparound]],\n" + + " Axis[“Latitude (φ)”, north, AxisMinValue[-100.0], AxisMaxValue[100.0], RangeMeaning[exact]],\n" + " Unit[“grad”, 0.015707963267948967, Id[“EPSG”, 9105]]],\n" + " Conversion[“Lambert zone II”,\n" + " Method[“Lambert Conic Conformal (1SP)”, Id[“EPSG”, 9801], Id[“GeoTIFF”, 9]],\n" + diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java index 4c5cc5e087..cbd10b0438 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/DefaultCoordinateSystemAxisTest.java @@ -88,25 +88,28 @@ public final class DefaultCoordinateSystemAxisTest extends TestCase { */ @Test public void testWKT() { - assertWktEquals(Convention.WKT2, "AXIS[“x”, east, LENGTHUNIT[“metre”, 1]]", X); - assertWktEquals(Convention.WKT2, "AXIS[“y”, north, LENGTHUNIT[“metre”, 1]]", Y); - assertWktEquals(Convention.WKT2, "AXIS[“z”, up, LENGTHUNIT[“metre”, 1]]", Z); - assertWktEquals(Convention.WKT2, "AXIS[“Longitude (λ)”, east, ANGLEUNIT[“grad”, 0.015707963267948967]]", LONGITUDE_gon); - assertWktEquals(Convention.WKT2, "AXIS[“Latitude (φ)”, north, ANGLEUNIT[“grad”, 0.015707963267948967]]", LATITUDE_gon); - assertWktEquals(Convention.WKT2, "AXIS[“Altitude (h)”, up, LENGTHUNIT[“metre”, 1]]", ALTITUDE); - assertWktEquals(Convention.WKT2, "AXIS[“Time (t)”, future, TIMEUNIT[“day”, 86400]]", TIME); - assertWktEquals(Convention.WKT2, "AXIS[“Longitude (λ)”, east, ANGLEUNIT[“degree”, 0.017453292519943295]]", GEODETIC_LONGITUDE); - assertWktEquals(Convention.WKT2, "AXIS[“Spherical longitude (θ)”, east, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LONGITUDE); - assertWktEquals(Convention.WKT2, "AXIS[“Latitude (φ)”, north, ANGLEUNIT[“degree”, 0.017453292519943295]]", GEODETIC_LATITUDE); - assertWktEquals(Convention.WKT2, "AXIS[“Spherical latitude (Ω)”, north, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LATITUDE); - - assertWktEquals(Convention.WKT1, "AXIS[“x”, EAST]", X); - assertWktEquals(Convention.WKT1, "AXIS[“y”, NORTH]", Y); - assertWktEquals(Convention.WKT1, "AXIS[“z”, UP]", Z); - assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic longitude (λ)”, east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", GEODETIC_LONGITUDE); - assertWktEquals(Convention.INTERNAL, "Axis[“Spherical longitude (θ)”, east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", SPHERICAL_LONGITUDE); - assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic latitude (φ)”, north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", GEODETIC_LATITUDE); - assertWktEquals(Convention.INTERNAL, "Axis[“Spherical latitude (Ω)”, north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", SPHERICAL_LATITUDE); + assertWktEquals(Convention.WKT2_2015, "AXIS[“x”, east, LENGTHUNIT[“metre”, 1]]", X); + assertWktEquals(Convention.WKT2_2015, "AXIS[“y”, north, LENGTHUNIT[“metre”, 1]]", Y); + assertWktEquals(Convention.WKT2_2015, "AXIS[“z”, up, LENGTHUNIT[“metre”, 1]]", Z); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Longitude (λ)”, east, ANGLEUNIT[“grad”, 0.015707963267948967]]", LONGITUDE_gon); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Latitude (φ)”, north, ANGLEUNIT[“grad”, 0.015707963267948967]]", LATITUDE_gon); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Altitude (h)”, up, LENGTHUNIT[“metre”, 1]]", ALTITUDE); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Time (t)”, future, TIMEUNIT[“day”, 86400]]", TIME); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Longitude (λ)”, east, ANGLEUNIT[“degree”, 0.017453292519943295]]", GEODETIC_LONGITUDE); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Spherical longitude (θ)”, east, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LONGITUDE); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Latitude (φ)”, north, ANGLEUNIT[“degree”, 0.017453292519943295]]", GEODETIC_LATITUDE); + assertWktEquals(Convention.WKT2_2015, "AXIS[“Spherical latitude (Ω)”, north, ANGLEUNIT[“degree”, 0.017453292519943295]]", SPHERICAL_LATITUDE); + assertWktEquals(Convention.WKT1, "AXIS[“x”, EAST]", X); + assertWktEquals(Convention.WKT1, "AXIS[“y”, NORTH]", Y); + assertWktEquals(Convention.WKT1, "AXIS[“z”, UP]", Z); + assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic longitude (λ)”, east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", GEODETIC_LONGITUDE); + assertWktEquals(Convention.INTERNAL, "Axis[“Spherical longitude (θ)”, east, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", SPHERICAL_LONGITUDE); + assertWktEquals(Convention.INTERNAL, "Axis[“Geodetic latitude (φ)”, north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", GEODETIC_LATITUDE); + assertWktEquals(Convention.INTERNAL, "Axis[“Spherical latitude (Ω)”, north, Unit[“degree”, 0.017453292519943295, Id[“EPSG”, 9102]]]", SPHERICAL_LATITUDE); + assertWktEquals(Convention.WKT2_2019, "AXIS[“Longitude (λ)”, east, ANGLEUNIT[“grad”, 0.015707963267948967], " + + "AXISMINVALUE[-200.0], AXISMAXVALUE[200.0], RANGEMEANING[wraparound]]", LONGITUDE_gon); + assertWktEquals(Convention.WKT2_2019, "AXIS[“Latitude (φ)”, north, ANGLEUNIT[“grad”, 0.015707963267948967], " + + "AXISMINVALUE[-100.0], AXISMAXVALUE[100.0], RANGEMEANING[exact]]", LATITUDE_gon); } /**
