| /* |
| * 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.referencing.cs; |
| |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Objects; |
| import javax.measure.Unit; |
| import javax.measure.quantity.Angle; |
| import javax.measure.UnitConverter; |
| import javax.xml.bind.annotation.XmlType; |
| import javax.xml.bind.annotation.XmlElement; |
| import javax.xml.bind.annotation.XmlAttribute; |
| import javax.xml.bind.annotation.XmlRootElement; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.referencing.cs.RangeMeaning; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.opengis.referencing.cs.PolarCS; |
| import org.opengis.referencing.cs.SphericalCS; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.CoordinateSystemAxis; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.apache.sis.internal.metadata.AxisNames; |
| import org.apache.sis.internal.referencing.WKTKeywords; |
| import org.apache.sis.internal.referencing.AxisDirections; |
| import org.apache.sis.internal.metadata.MetadataUtilities; |
| import org.apache.sis.referencing.AbstractIdentifiedObject; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.measure.Longitude; |
| import org.apache.sis.measure.Latitude; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.internal.jaxb.Context; |
| import org.apache.sis.io.wkt.Formatter; |
| 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 java.lang.Double.doubleToLongBits; |
| import static java.lang.Double.NEGATIVE_INFINITY; |
| import static java.lang.Double.POSITIVE_INFINITY; |
| import static org.apache.sis.util.ArgumentChecks.*; |
| import static org.apache.sis.util.CharSequences.trimWhitespaces; |
| import static org.apache.sis.util.collection.Containers.property; |
| |
| /* |
| * The identifier for axis of unknown name. We have to use this identifier when the axis direction changed, |
| * because such change often implies a name change too (e.g. "Westing" → "Easting"), and we can not always |
| * guess what the new name should be. This constant is used as a sentinel value set by Normalizer and checked |
| * by DefaultCoordinateSystemAxis for skipping axis name comparisons when the axis name is unknown. |
| */ |
| import static org.apache.sis.internal.referencing.NilReferencingObject.UNNAMED; |
| |
| |
| /** |
| * Coordinate system axis name, direction, unit and range of values. |
| * |
| * <div class="section">Axis names</div> |
| * In some case, the axis name is constrained by ISO 19111 depending on the |
| * {@linkplain org.opengis.referencing.crs.CoordinateReferenceSystem coordinate reference system} type. |
| * This constraint works in two directions. For example the names <cite>"geodetic latitude"</cite> and |
| * <cite>"geodetic longitude"</cite> shall be used to designate the coordinate axis names associated |
| * with a {@link org.opengis.referencing.crs.GeographicCRS}. Conversely, these names shall not be used |
| * in any other context. See the GeoAPI {@link CoordinateSystemAxis} javadoc for more information. |
| * |
| * <div class="section">Immutability and thread safety</div> |
| * This class is immutable and thus thread-safe if the property <em>values</em> (not necessarily the map itself) |
| * given to the constructor are also immutable. Unless otherwise noted in the javadoc, this condition holds if all |
| * components were created using only SIS factories and static constants. |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 0.8 |
| * |
| * @see AbstractCS |
| * @see Unit |
| * |
| * @since 0.4 |
| * @module |
| */ |
| @XmlType(name = "CoordinateSystemAxisType", propOrder = { |
| "abbreviation", |
| "direction", |
| "minimum", |
| "maximum", |
| "rangeMeaning" |
| }) |
| @XmlRootElement(name = "CoordinateSystemAxis") |
| public class DefaultCoordinateSystemAxis extends AbstractIdentifiedObject implements CoordinateSystemAxis { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = -7883614853277827689L; |
| |
| /** |
| * Key for the <code>{@value}</code> property to be given to the constructor. |
| * This is used for setting the value to be returned by {@link #getMinimumValue()}. |
| */ |
| public static final String MINIMUM_VALUE_KEY = "minimumValue"; |
| |
| /** |
| * Key for the <code>{@value}</code> property to be given to the constructor. |
| * This is used for setting the value to be returned by {@link #getMaximumValue()}. |
| */ |
| public static final String MAXIMUM_VALUE_KEY = "maximumValue"; |
| |
| /** |
| * Key for the <code>{@value}</code> property to be given to the constructor. |
| * This is used for setting the value to be returned by {@link #getRangeMeaning()}. |
| */ |
| public static final String RANGE_MEANING_KEY = "rangeMeaning"; |
| |
| /** |
| * Some names to be treated as equivalent. This is needed because axis names are the primary way to |
| * distinguish between {@link CoordinateSystemAxis} instances. Those names are strictly defined by |
| * ISO 19111 as "Geodetic latitude" and "Geodetic longitude" among others, but the legacy WKT |
| * specifications from OGC 01-009 defined the names as "Lon" and "Lat" for the same axis. |
| * |
| * <p>Keys in this map are names <strong>in lower cases</strong>. |
| * Values are any object that allow us to differentiate latitude from longitude.</p> |
| * |
| * <p>Similar strings appear in {@link #formatTo(Formatter)} and |
| * {@code org.apache.sis.io.wkt.GeodeticObjectParser.parseAxis(…)}.</p> |
| * |
| * @see #isHeuristicMatchForName(String) |
| */ |
| private static final Map<String,Object> ALIASES = new HashMap<>(12); |
| static { |
| final Boolean latitude = Boolean.TRUE; |
| final Boolean longitude = Boolean.FALSE; |
| ALIASES.put("lat", latitude); |
| ALIASES.put("latitude", latitude); |
| ALIASES.put("geodetic latitude", latitude); |
| ALIASES.put("lon", longitude); |
| ALIASES.put("long", longitude); |
| ALIASES.put("longitude", longitude); |
| ALIASES.put("geodetic longitude", longitude); |
| /* |
| * Do not add aliases for "x" and "y" in this map. See ALIASES_XY for more information. |
| */ |
| } |
| |
| /** |
| * Aliases for the "x" and "y" abbreviations (special cases). "x" and "y" are sometime used (especially in WKT) |
| * for meaning "Easting" and "Northing". However we shall not add "x" and "y" as aliases in the {@link #ALIASES} |
| * map, because experience has shown that doing so cause a lot of undesirable side effects. The "x" abbreviation |
| * is used for too many things ("Easting", "Westing", "Geocentric X", "Display right", "Display left") and likewise |
| * for "y". Declaring them as aliases introduces confusion in many places. Instead, the "x" and "y" cases are |
| * handled in a special way by the {@code isHeuristicMatchForNameXY(…)} method. |
| * |
| * <p>Names at even index are for "x" and names at odd index are for "y".</p> |
| * |
| * @see #isHeuristicMatchForNameXY(String, String) |
| */ |
| private static final String[] ALIASES_XY = { |
| AxisNames.EASTING, AxisNames.NORTHING, |
| AxisNames.WESTING, AxisNames.SOUTHING |
| }; |
| |
| /** |
| * The abbreviation used for this coordinate system axes. |
| * Examples are <cite>"X"</cite> and <cite>"Y"</cite>. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setAbbreviation(String)}</p> |
| * |
| * @see #getAbbreviation() |
| */ |
| private String abbreviation; |
| |
| /** |
| * Direction of this coordinate system axis. In the case of Cartesian projected |
| * coordinates, this is the direction of this coordinate system axis locally. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setDirection(AxisDirection)}</p> |
| * |
| * @see #getDirection() |
| */ |
| private AxisDirection direction; |
| |
| /** |
| * The unit of measure used for this coordinate system axis. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setUnit(Unit)}</p> |
| * |
| * @see #getUnit() |
| */ |
| private Unit<?> unit; |
| |
| /** |
| * Minimal and maximal value for this axis, or negative/positive infinity if none. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setMinimum(Double)} |
| * or {@link #setMaximum(Double)}</p> |
| */ |
| private double minimumValue, maximumValue; |
| |
| /** |
| * The range meaning for this axis, or {@code null} if unspecified. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setRangeMeaning(RangeMeaning)}</p> |
| * |
| * @see #getRangeMeaning() |
| */ |
| private RangeMeaning rangeMeaning; |
| |
| /** |
| * Constructs an axis from a set of properties. The properties given in argument follow the same rules |
| * than for the {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}. |
| * Additionally, the following properties are understood by this constructor: |
| * |
| * <table class="sis"> |
| * <caption>Recognized properties (non exhaustive list)</caption> |
| * <tr> |
| * <th>Property name</th> |
| * <th>Value type</th> |
| * <th>Returned by</th> |
| * </tr> |
| * <tr> |
| * <td>{@value #MINIMUM_VALUE_KEY}</td> |
| * <td>{@link Number}</td> |
| * <td>{@link #getMinimumValue()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value #MAXIMUM_VALUE_KEY}</td> |
| * <td>{@link Number}</td> |
| * <td>{@link #getMaximumValue()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value #RANGE_MEANING_KEY}</td> |
| * <td>{@link RangeMeaning}</td> |
| * <td>{@link #getRangeMeaning()}</td> |
| * </tr> |
| * <tr> |
| * <th colspan="3" class="hsep">Defined in parent class (reminder)</th> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td> |
| * <td>{@link org.opengis.referencing.ReferenceIdentifier} or {@link String}</td> |
| * <td>{@link #getName()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td> |
| * <td>{@link GenericName} or {@link CharSequence} (optionally as array)</td> |
| * <td>{@link #getAlias()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td> |
| * <td>{@link org.opengis.referencing.ReferenceIdentifier} (optionally as array)</td> |
| * <td>{@link #getIdentifiers()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getRemarks()}</td> |
| * </tr> |
| * </table> |
| * |
| * Generally speaking, information provided in the {@code properties} map are considered ignorable metadata |
| * (except the axis name) while information provided as explicit arguments may have an impact on coordinate |
| * transformation results. Exceptions to this rule are the {@code minimumValue} and {@code maximumValue} in |
| * the particular case where {@code rangeMeaning} is {@link RangeMeaning#WRAPAROUND}. |
| * |
| * <p>If no minimum, maximum and range meaning are specified, then this constructor will infer them |
| * from the axis unit and direction.</p> |
| * |
| * @param properties the properties to be given to the identified object. |
| * @param abbreviation the {@linkplain #getAbbreviation() abbreviation} used for this coordinate system axis. |
| * @param direction the {@linkplain #getDirection() direction} of this coordinate system axis. |
| * @param unit the {@linkplain #getUnit() unit of measure} used for this coordinate system axis. |
| * |
| * @see org.apache.sis.referencing.factory.GeodeticObjectFactory#createCoordinateSystemAxis(Map, String, AxisDirection, Unit) |
| */ |
| public DefaultCoordinateSystemAxis(final Map<String,?> properties, |
| final String abbreviation, |
| final AxisDirection direction, |
| final Unit<?> unit) |
| { |
| super(properties); |
| 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); |
| if (minimum == null && maximum == null && rm == null) { |
| double min = Double.NEGATIVE_INFINITY; |
| double max = Double.POSITIVE_INFINITY; |
| if (Units.isAngular(unit)) { |
| final UnitConverter fromDegrees = Units.DEGREE.getConverterTo(unit.asType(Angle.class)); |
| final AxisDirection dir = AxisDirections.absolute(direction); |
| if (dir.equals(AxisDirection.NORTH)) { |
| min = fromDegrees.convert(Latitude.MIN_VALUE); |
| max = fromDegrees.convert(Latitude.MAX_VALUE); |
| rm = RangeMeaning.EXACT; |
| } else if (dir.equals(AxisDirection.EAST)) { |
| min = fromDegrees.convert(Longitude.MIN_VALUE); |
| max = fromDegrees.convert(Longitude.MAX_VALUE); |
| rm = RangeMeaning.WRAPAROUND; // 180°E wraps to 180°W |
| } |
| if (min > max) { |
| final double t = min; |
| min = max; |
| max = t; |
| } |
| } |
| minimumValue = min; |
| maximumValue = max; |
| } else { |
| minimumValue = (minimum != null) ? minimum.doubleValue() : Double.NEGATIVE_INFINITY; |
| maximumValue = (maximum != null) ? maximum.doubleValue() : Double.POSITIVE_INFINITY; |
| if (!(minimumValue < maximumValue)) { // Use '!' for catching NaN |
| throw new IllegalArgumentException(Errors.getResources(properties).getString( |
| Errors.Keys.IllegalRange_2, minimumValue, maximumValue)); |
| } |
| if ((minimumValue != NEGATIVE_INFINITY) || (maximumValue != POSITIVE_INFINITY)) { |
| ensureNonNull(RANGE_MEANING_KEY, rm); |
| } else { |
| rm = null; |
| } |
| } |
| rangeMeaning = rm; |
| } |
| |
| /** |
| * Creates a new coordinate system axis with the same values than the specified one. |
| * This copy constructor provides a way to convert an arbitrary implementation into a SIS one |
| * or a user-defined one (as a subclass), usually in order to leverage some implementation-specific API. |
| * |
| * <p>This constructor performs a shallow copy, i.e. the properties are not cloned.</p> |
| * |
| * @param axis the coordinate system axis to copy. |
| * |
| * @see #castOrCopy(CoordinateSystemAxis) |
| */ |
| protected DefaultCoordinateSystemAxis(final CoordinateSystemAxis axis) { |
| super(axis); |
| abbreviation = axis.getAbbreviation(); |
| direction = axis.getDirection(); |
| unit = axis.getUnit(); |
| minimumValue = axis.getMinimumValue(); |
| maximumValue = axis.getMaximumValue(); |
| rangeMeaning = axis.getRangeMeaning(); |
| } |
| |
| /** |
| * Returns a SIS axis implementation with the same values than the given arbitrary implementation. |
| * If the given object is {@code null}, then this method returns {@code null}. Otherwise if the |
| * given object is already a SIS implementation, then the given object is returned unchanged. |
| * Otherwise a new SIS implementation is created and initialized to the values of the given object. |
| * |
| * @param object the object to get as a SIS implementation, or {@code null} if none. |
| * @return a SIS implementation containing the values of the given object (may be the |
| * given object itself), or {@code null} if the argument was null. |
| */ |
| public static DefaultCoordinateSystemAxis castOrCopy(final CoordinateSystemAxis object) { |
| return (object == null) || (object instanceof DefaultCoordinateSystemAxis) |
| ? (DefaultCoordinateSystemAxis) object : new DefaultCoordinateSystemAxis(object); |
| } |
| |
| /** |
| * Returns the GeoAPI interface implemented by this class. |
| * The SIS implementation returns {@code CoordinateSystemAxis.class}. |
| * |
| * <div class="note"><b>Note for implementers:</b> |
| * Subclasses usually do not need to override this method since GeoAPI does not define {@code CoordinateSystemAxis} |
| * sub-interface. Overriding possibility is left mostly for implementers who wish to extend GeoAPI with their own |
| * set of interfaces.</div> |
| * |
| * @return {@code CoordinateSystemAxis.class} or a user-defined sub-interface. |
| */ |
| @Override |
| public Class<? extends CoordinateSystemAxis> getInterface() { |
| return CoordinateSystemAxis.class; |
| } |
| |
| /** |
| * Returns the direction of this coordinate system axis. |
| * This direction is often approximate and intended to provide a human interpretable meaning to the axis. |
| * A {@linkplain AbstractCS coordinate system} can not contain two axes having the same direction or |
| * opposite directions. |
| * |
| * <p>Examples: |
| * {@linkplain AxisDirection#NORTH north} or {@linkplain AxisDirection#SOUTH south}, |
| * {@linkplain AxisDirection#EAST east} or {@linkplain AxisDirection#WEST west}, |
| * {@linkplain AxisDirection#UP up} or {@linkplain AxisDirection#DOWN down}.</p> |
| * |
| * @return the direction of this coordinate system axis. |
| */ |
| @Override |
| @XmlElement(name = "axisDirection", required = true) |
| public AxisDirection getDirection() { |
| return direction; |
| } |
| |
| /** |
| * Returns the abbreviation used for this coordinate system axes. |
| * Examples are <cite>"X"</cite> and <cite>"Y"</cite>. |
| * |
| * @return the coordinate system axis abbreviation. |
| */ |
| @Override |
| @XmlElement(name = "axisAbbrev", required = true) |
| public String getAbbreviation() { |
| return abbreviation; |
| } |
| |
| /** |
| * Returns the unit of measure used for this coordinate system axis. If this {@code CoordinateSystemAxis} was |
| * given by <code>{@link AbstractCS#getAxis(int) CoordinateSystem.getAxis}(i)</code>, then all coordinate |
| * values at dimension <var>i</var> in a coordinate tuple shall be recorded using this unit of measure. |
| * |
| * @return the unit of measure used for coordinate values along this coordinate system axis. |
| */ |
| @Override |
| @XmlAttribute(name= "uom", required = true) |
| public Unit<?> getUnit() { |
| return unit; |
| } |
| |
| /** |
| * Returns the minimum value normally allowed for this axis, in the {@linkplain #getUnit() |
| * unit of measure for the axis}. If there is no minimum value, then this method returns |
| * {@linkplain Double#NEGATIVE_INFINITY negative infinity}. |
| * |
| * @return the minimum value normally allowed for this axis. |
| */ |
| @Override |
| public double getMinimumValue() { |
| return minimumValue; |
| } |
| |
| /** |
| * Returns the maximum value normally allowed for this axis, in the {@linkplain #getUnit() |
| * unit of measure for the axis}. If there is no maximum value, then this method returns |
| * {@linkplain Double#POSITIVE_INFINITY negative infinity}. |
| * |
| * @return the maximum value normally allowed for this axis. |
| */ |
| @Override |
| public double getMaximumValue() { |
| return maximumValue; |
| } |
| |
| /** |
| * Invoked at unmarshalling time if a minimum or maximum value is out of range. |
| * |
| * @param name the property name. Will also be used as "method" name for logging purpose, |
| * since the setter method "conceptually" do not exist (it is only for JAXB). |
| * @param value the invalid value. |
| */ |
| private static void outOfRange(final String name, final Double value) { |
| Context.warningOccured(Context.current(), DefaultCoordinateSystemAxis.class, name, |
| Errors.class, Errors.Keys.InconsistentAttribute_2, name, value); |
| } |
| |
| /** |
| * Returns the meaning of axis value range specified by the {@linkplain #getMinimumValue() minimum} |
| * and {@linkplain #getMaximumValue() maximum} values. If there is no minimum and maximum values |
| * (i.e. if those values are {@linkplain Double#NEGATIVE_INFINITY negative infinity} and |
| * {@linkplain Double#POSITIVE_INFINITY positive infinity} respectively), then this method returns {@code null}. |
| * |
| * @return the meaning of axis value range, or {@code null} if unspecified. |
| */ |
| @Override |
| @XmlElement(name = "rangeMeaning") |
| public RangeMeaning getRangeMeaning() { |
| return rangeMeaning; |
| } |
| |
| /** |
| * Returns {@code true} if either the {@linkplain #getName() primary name} or at least |
| * one {@linkplain #getAlias() alias} matches the given string according heuristic rules. |
| * This method performs the comparison documented in the |
| * {@link AbstractIdentifiedObject#isHeuristicMatchForName(String) super-class}, |
| * with an additional flexibility for latitudes and longitudes: |
| * |
| * <ul> |
| * <li>{@code "Lat"}, {@code "Latitude"} and {@code "Geodetic latitude"} are considered equivalent.</li> |
| * <li>{@code "Lon"}, {@code "Longitude"} and {@code "Geodetic longitude"} are considered equivalent.</li> |
| * </ul> |
| * |
| * The above special cases are needed in order to workaround a conflict in specifications: |
| * ISO 19111 states explicitly that the latitude and longitude axis names shall be |
| * <cite>"Geodetic latitude"</cite> and <cite>"Geodetic longitude"</cite>, while the legacy |
| * OGC 01-009 (where version 1 of the WKT format is defined) said that the default values shall be |
| * <cite>"Lat"</cite> and <cite>"Lon"</cite>. |
| * |
| * <div class="section">Future evolutions</div> |
| * This method implements heuristic rules learned from experience while trying to provide inter-operability |
| * with different data producers. Those rules may be adjusted in any future SIS version according experience |
| * gained while working with more data producers. |
| * |
| * @param name the name to compare. |
| * @return {@code true} if the primary name of at least one alias matches the specified {@code name}. |
| */ |
| @Override |
| public boolean isHeuristicMatchForName(final String name) { |
| if (super.isHeuristicMatchForName(name)) { |
| return true; |
| } |
| /* |
| * The standard comparisons didn't worked. Check for the aliases. Note: we don't test |
| * for 'isHeuristicMatchForNameXY(...)' here because the "x" and "y" axis names are |
| * too generic. We test them only in the 'equals' method, which has the extra-safety |
| * of units comparison (so less risk to treat incompatible axes as equivalent). |
| */ |
| final Object type = ALIASES.get(trimWhitespaces(name).toLowerCase(Locale.US)); // Our ALIASES are in English. |
| return (type != null) && (type == ALIASES.get(trimWhitespaces(getName().getCode()).toLowerCase(Locale.US))); |
| } |
| |
| /** |
| * Special cases for "x" and "y" names. "x" is considered equivalent to "Easting" or "Westing", |
| * but the converse is not true. Note: by avoiding to put "x" in the {@link #ALIASES} map, we |
| * avoid undesirable side effects like considering "Easting" as equivalent to "Westing". |
| * |
| * @param xy the name which may be "x" or "y". |
| * @param name the second name to compare with. |
| * @return {@code true} if the second name is equivalent to "x" or "y" |
| * (depending on the {@code xy} value), or {@code false} otherwise. |
| */ |
| private static boolean isHeuristicMatchForNameXY(String xy, String name) { |
| xy = trimWhitespaces(xy); |
| if (xy.length() == 1) { |
| int i = Character.toLowerCase(xy.charAt(0)) - 'x'; |
| if (i >= 0 && i <= 1) { |
| name = trimWhitespaces(name); |
| if (!name.isEmpty()) do { |
| if (name.regionMatches(true, 0, ALIASES_XY[i], 0, name.length())) { |
| return true; |
| } |
| } while ((i += 2) < ALIASES_XY.length); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Compares the unit and direction of this axis with the ones of the given axis. |
| * The range minimum and maximum values are compared only if {@code cr} is {@code true}, |
| * i.e. it is caller responsibility to determine if range shall be considered as metadata. |
| * |
| * @param that the axis to compare with this axis. |
| * @param mode whether the unit comparison is an approximation or exact. |
| * @param cr {@code true} for comparing also the range minimum and maximum values. |
| * @return {@code true} if unit, direction and optionally range extremum are equal. |
| */ |
| private boolean equalsIgnoreMetadata(final CoordinateSystemAxis that, final ComparisonMode mode, final boolean cr) { |
| return Objects.equals(getDirection(), that.getDirection()) && |
| Utilities.deepEquals(getUnit(), that.getUnit(), mode) && |
| (!cr || (doubleToLongBits(getMinimumValue()) == doubleToLongBits(that.getMinimumValue()) && |
| doubleToLongBits(getMaximumValue()) == doubleToLongBits(that.getMaximumValue()))); |
| } |
| |
| /** |
| * Compares the specified object with this axis for equality. |
| * The strictness level is controlled by the second argument. |
| * This method compares the following properties in every cases: |
| * |
| * <ul> |
| * <li>{@link #getName()}</li> |
| * <li>{@link #getDirection()}</li> |
| * <li>{@link #getUnit()}</li> |
| * </ul> |
| * |
| * In the particular case where {@link #getRangeMeaning()} is {@code WRAPAROUND}, then {@link #getMinimumValue()} |
| * and {@link #getMaximumValue()} are considered non-ignorable metadata and will be compared for every modes. |
| * All other properties are compared only for modes stricter than {@link ComparisonMode#IGNORE_METADATA}. |
| * |
| * @param object the object to compare to {@code this}. |
| * @param mode {@link ComparisonMode#STRICT STRICT} for performing a strict comparison, or |
| * {@link ComparisonMode#IGNORE_METADATA IGNORE_METADATA} for comparing only |
| * properties relevant to coordinate transformations. |
| * @return {@code true} if both objects are equal. |
| */ |
| @Override |
| public boolean equals(final Object object, final ComparisonMode mode) { |
| if (object == this) { |
| return true; // Slight optimization. |
| } |
| if (!super.equals(object, mode)) { |
| return false; |
| } |
| switch (mode) { |
| case STRICT: { |
| final DefaultCoordinateSystemAxis that = (DefaultCoordinateSystemAxis) object; |
| return Objects.equals(unit, that.unit) && |
| Objects.equals(direction, that.direction) && |
| Objects.equals(abbreviation, that.abbreviation) && |
| Objects.equals(rangeMeaning, that.rangeMeaning) && |
| doubleToLongBits(minimumValue) == doubleToLongBits(that.minimumValue) && |
| doubleToLongBits(maximumValue) == doubleToLongBits(that.maximumValue); |
| } |
| case BY_CONTRACT: { |
| final CoordinateSystemAxis that = (CoordinateSystemAxis) object; |
| return equalsIgnoreMetadata(that, mode, true) && |
| Objects.equals(getAbbreviation(), that.getAbbreviation()) && |
| Objects.equals(getRangeMeaning(), that.getRangeMeaning()); |
| } |
| } |
| /* |
| * At this point the comparison is in "ignore metadata" mode. We compare the axis range |
| * only if the range meaning is "wraparound" for both axes, because only in such case a |
| * coordinate operation may shift some coordinate values (typically ±360° on longitudes). |
| */ |
| final CoordinateSystemAxis that = (CoordinateSystemAxis) object; |
| if (!equalsIgnoreMetadata(that, mode, RangeMeaning.WRAPAROUND.equals(this.getRangeMeaning()) && |
| RangeMeaning.WRAPAROUND.equals(that.getRangeMeaning()))) |
| { |
| return false; |
| } |
| Identifier name = that.getName(); |
| if (name != UNNAMED) { |
| /* |
| * Checking the abbreviation is not sufficient. For example the polar angle and the |
| * spherical latitude have the same abbreviation (θ). Legacy names like "Longitude" |
| * (in addition to ISO 19111 "Geodetic longitude") bring more potential confusion. |
| * Furthermore, not all implementers use the greek letters. For example most CRS in |
| * WKT format use the "Lat" abbreviation instead of the greek letter φ. |
| * For comparisons without metadata, we ignore the unreliable abbreviation and check |
| * the axis name instead. These names are constrained by ISO 19111 specification |
| * (see class javadoc), so they should be reliable enough. |
| * |
| * Note: there is no need to execute this block if metadata are not ignored, |
| * because in this case a stricter check has already been performed by |
| * the 'equals' method in the superclass. |
| */ |
| final String thatCode = name.getCode(); |
| if (!isHeuristicMatchForName(thatCode)) { |
| name = getName(); |
| if (name != UNNAMED) { |
| /* |
| * The above test checked for special cases ("Lat" / "Lon" aliases, etc.). |
| * The next line may repeat the same check, so we may have a partial waste |
| * of CPU. But we do it anyway for checking the 'that' aliases, and also |
| * because the user may have overridden 'that.isHeuristicMatchForName(…)'. |
| */ |
| final String thisCode = name.getCode(); |
| if (!IdentifiedObjects.isHeuristicMatchForName(that, thisCode)) { |
| // Check for the special case of "x" and "y" axis names. |
| if (!isHeuristicMatchForNameXY(thatCode, thisCode) && |
| !isHeuristicMatchForNameXY(thisCode, thatCode)) |
| { |
| return false; |
| } |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Invoked by {@code hashCode()} for computing the hash code when first needed. |
| * See {@link org.apache.sis.referencing.AbstractIdentifiedObject#computeHashCode()} |
| * for more information. |
| * |
| * @return the hash code value. This value may change in any future Apache SIS version. |
| */ |
| @Override |
| protected long computeHashCode() { |
| return super.computeHashCode() + Objects.hashCode(unit) + Objects.hashCode(direction) |
| + doubleToLongBits(minimumValue) + 31*doubleToLongBits(maximumValue); |
| } |
| |
| /** |
| * Returns the enclosing coordinate system, or {@code null} if none. In ISO 19162 compliant WKT the coordinate |
| * <strong>reference</strong> system should be the first parent ({@code formatter.getEnclosingElement(1)}) and |
| * the coordinate system shall be obtained from that CRS (yes, this is convolved. This is because of historical |
| * reasons, since compatibility with WKT 1 was a requirement of WKT 2). |
| */ |
| private static CoordinateSystem getEnclosingCS(final Formatter formatter) { |
| final FormattableObject e = formatter.getEnclosingElement(1); |
| if (e instanceof CoordinateReferenceSystem) { // This is what we expect in standard WKT. |
| return ((CoordinateReferenceSystem) e).getCoordinateSystem(); |
| } |
| if (e instanceof CoordinateSystem) { // Not standard WKT, but conceptually the right thing. |
| return (CoordinateSystem) e; |
| } |
| return null; |
| } |
| |
| /** |
| * Formats this axis as a <cite>Well Known Text</cite> {@code Axis[…]} element. |
| * |
| * <div class="section">Constraints for WKT validity</div> |
| * The ISO 19162 specification puts many constraints on axis names, abbreviations and directions allowed in WKT. |
| * Most of those constraints are inherited from ISO 19111 — see {@link CoordinateSystemAxis} javadoc for some of |
| * those. The current Apache SIS implementation does not verify whether this axis name and abbreviation are |
| * compliant; we assume that the user created a valid axis. |
| * The only actions (derived from ISO 19162 rules) taken by this method (by default) are: |
| * |
| * <ul> |
| * <li>Replace <cite>“Geodetic latitude”</cite> and <cite>“Geodetic longitude”</cite> names (case insensitive) |
| * by <cite>“latitude”</cite> and <cite>“longitude”</cite> respectively.</li> |
| * <li>For latitude and longitude axes, replace “φ” and “λ” abbreviations by <var>“B”</var> and <var>“L”</var> |
| * respectively (from German “Breite” and “Länge”, used in academic texts worldwide). |
| * Note that <var>“L”</var> is also the transliteration of Greek letter “lambda” (λ).</li> |
| * <li>In {@link SphericalCS}, replace “φ” and “θ” abbreviations by <var>“U”</var> and <var>“V”</var> respectively.</li> |
| * <li>In {@link PolarCS}, replace “θ” abbreviation by <var>“U”</var>.</li> |
| * </ul> |
| * |
| * The above-cited replacements of name and Greek letters can be controlled by a call to |
| * {@link org.apache.sis.io.wkt.WKTFormat#setTransliterator(Transliterator)}. |
| * |
| * @return {@code "Axis"}. |
| * |
| * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#39">WKT 2 specification §7.5.3</a> |
| */ |
| @Override |
| protected String formatTo(final Formatter formatter) { |
| final Convention convention = formatter.getConvention(); |
| final boolean isWKT1 = (convention.majorVersion() == 1); |
| final boolean isInternal = (convention == Convention.INTERNAL); |
| final CoordinateSystem cs = getEnclosingCS(formatter); |
| AxisDirection dir = getDirection(); |
| String name = IdentifiedObjects.getName(this, formatter.getNameAuthority()); |
| if (name == null) { |
| name = IdentifiedObjects.getName(this, null); |
| } |
| if (name != null && !isInternal) { |
| final String old = name; |
| name = formatter.getTransliterator().toShortAxisName(cs, dir, name); |
| if (name == null && isWKT1) { |
| name = old; // WKT 1 does not allow omission of name. |
| } |
| } |
| /* |
| * ISO 19162:2015 §7.5.3 suggests to put abbreviation in parentheses, e.g. "Easting (x)". |
| * The specification also suggests to write only the abbreviation (e.g. "(X)") in the |
| * special case of Geocentric axis, and disallows Greek letters. |
| */ |
| if (!isWKT1) { |
| final String a = formatter.getTransliterator().toLatinAbbreviation(cs, dir, getAbbreviation()); |
| if (a != null && !a.equals(name)) { |
| final StringBuilder buffer = new StringBuilder(); |
| if (name != null) { |
| buffer.append(name).append(' '); |
| } |
| name = buffer.append('(').append(a).append(')').toString(); |
| } |
| } |
| formatter.append(name, ElementKind.AXIS); |
| /* |
| * Format the axis direction, optionally followed by a MERIDIAN[…] element |
| * if the direction is of the kind "South along 90°N" for instance. |
| */ |
| DirectionAlongMeridian meridian = null; |
| if (AxisDirections.isUserDefined(dir)) { |
| meridian = DirectionAlongMeridian.parse(dir); |
| if (meridian != null) { |
| dir = meridian.baseDirection; |
| if (isWKT1) { |
| formatter.setInvalidWKT(this, null); |
| } |
| } |
| } |
| formatter.append(dir); |
| formatter.append(meridian); |
| /* |
| * Formats the axis unit only if the enclosing CRS element does not provide one. |
| * If the enclosing CRS provided a contextual unit, then it is assumed to apply |
| * to all axes (we do not verify). |
| */ |
| if (!isWKT1) { |
| if (convention == Convention.WKT2 && cs != null) { |
| final Order order = Order.create(cs, this); |
| if (order != null) { |
| formatter.append(order); |
| } else { |
| formatter.setInvalidWKT(cs, null); |
| } |
| } |
| if (!formatter.hasContextualUnit(1)) { |
| formatter.append(getUnit()); |
| } |
| } |
| return WKTKeywords.Axis; |
| } |
| |
| /** |
| * The {@code ORDER[…]} element to be formatted inside {@code AXIS[…]} element. |
| * This is an element of WKT 2 only. |
| */ |
| private static final class Order extends FormattableObject { |
| /** |
| * The sequence number to format inside the {@code ORDER[…]} element. |
| */ |
| private final int index; |
| |
| /** |
| * Creates a new {@code ORDER[…]} element for the given axis in the given coordinate system. |
| * If this method does not found exactly one instance of the given axis in the given coordinate system, |
| * then returns {@code null}. In the later case, it is caller's responsibility to declare the WKT as invalid. |
| * |
| * <p>This method is a little bit inefficient since the enclosing {@link AbstractCS#formatTo(Formatter)} |
| * method already know this axis index. But there is currently no API in {@link Formatter} for carrying |
| * this information, and we are a little bit reluctant to introduce such API since it would force us to |
| * introduce lists in a model which is, for everything else, purely based on trees.</p> |
| * |
| * @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; |
| final int dimension = cs.getDimension(); |
| for (int i=0; i<dimension;) { |
| if (cs.getAxis(i++) == axis) { |
| if (order == null) { |
| order = new Order(i); |
| } else { |
| return null; |
| } |
| } |
| } |
| return order; |
| } |
| |
| /** |
| * Creates new {@code ORDER[…]} element for the given sequential number. |
| */ |
| private Order(final int index) { |
| this.index = index; |
| } |
| |
| /** |
| * Formats the {@code ORDER[…]} element. |
| */ |
| @Override |
| protected String formatTo(final Formatter formatter) { |
| formatter.append(index); |
| return WKTKeywords.Order; |
| } |
| } |
| |
| |
| |
| |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| //////// //////// |
| //////// XML support with JAXB //////// |
| //////// //////// |
| //////// The following methods are invoked by JAXB using reflection (even if //////// |
| //////// they are private) or are helpers for other methods invoked by JAXB. //////// |
| //////// Those methods can be safely removed if Geographic Markup Language //////// |
| //////// (GML) support is not needed. //////// |
| //////// //////// |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Constructs a new object in which every attributes are set to a null value. |
| * <strong>This is not a valid object.</strong> This constructor is strictly |
| * reserved to JAXB, which will assign values to the fields using reflexion. |
| */ |
| private DefaultCoordinateSystemAxis() { |
| super(org.apache.sis.internal.referencing.NilReferencingObject.INSTANCE); |
| minimumValue = NEGATIVE_INFINITY; |
| maximumValue = POSITIVE_INFINITY; |
| /* |
| * Direction and unit of measurement are mandatory for SIS working. We do not verify their presence here |
| * because the verification would have to be done in an 'afterMarshal(…)' method and throwing an exception |
| * in that method causes the whole unmarshalling to fail. But the CD_CoordinateSystemAxis adapter does some |
| * verifications. |
| */ |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time. |
| * |
| * @see #getAbbreviation() |
| */ |
| private void setAbbreviation(final String value) { |
| if (abbreviation == null) { |
| abbreviation = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setAbbreviation", "abbreviation"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time. |
| * |
| * @see #getDirection() |
| */ |
| private void setDirection(final AxisDirection value) { |
| if (direction == null) { |
| direction = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setDirection", "direction"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time. |
| * |
| * @see #getUnit() |
| */ |
| private void setUnit(final Unit<?> value) { |
| if (unit == null) { |
| unit = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setUnit", "unit"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time. |
| * |
| * @see #getRangeMeaning() |
| */ |
| private void setRangeMeaning(final RangeMeaning value) { |
| if (rangeMeaning == null) { |
| rangeMeaning = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setRangeMeaning", "rangeMeaning"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB at marshalling time for fetching the minimum value, or {@code null} if none. |
| * |
| * @see #getMinimumValue() |
| */ |
| @XmlElement(name = "minimumValue") |
| private Double getMinimum() { |
| return (minimumValue != NEGATIVE_INFINITY) ? minimumValue : null; |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time for setting the minimum value. |
| */ |
| private void setMinimum(final Double value) { |
| if (minimumValue == NEGATIVE_INFINITY) { |
| final double min = value; // Apply unboxing. |
| if (min < maximumValue) { |
| minimumValue = min; |
| } else { |
| outOfRange("minimumValue", value); |
| } |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setMinimum", "minimumValue"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB at marshalling time for fetching the maximum value, or {@code null} if none. |
| * |
| * @see #getMaximumValue() |
| */ |
| @XmlElement(name = "maximumValue") |
| private Double getMaximum() { |
| return (maximumValue != POSITIVE_INFINITY) ? maximumValue : null; |
| } |
| |
| /** |
| * Invoked by JAXB at unmarshalling time for setting the maximum value. |
| */ |
| private void setMaximum(final Double value) { |
| if (maximumValue == POSITIVE_INFINITY) { |
| final double max = value; // Apply unboxing. |
| if (max > minimumValue) { |
| maximumValue = max; |
| } else { |
| outOfRange("maximumValue", value); |
| } |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultCoordinateSystemAxis.class, "setMaximum", "maximumValue"); |
| } |
| } |
| } |