001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2008-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.test.geometry;
033
034import java.util.Arrays;
035
036import org.opengis.geometry.*;
037import org.opengis.referencing.cs.CoordinateSystem;
038import org.opengis.referencing.cs.CoordinateSystemAxis;
039import org.opengis.referencing.crs.CoordinateReferenceSystem;
040import org.opengis.referencing.cs.RangeMeaning;
041
042import org.opengis.test.Validator;
043import org.opengis.test.ValidatorContainer;
044
045import static java.lang.Double.NaN;
046import static java.lang.Double.isNaN;
047import static org.opengis.test.Assert.*;
048
049
050/**
051 * Validates {@link Geometry} and related objects from the {@code org.opengis.geometry}
052 * package.
053 *
054 * <p>This class is provided for users wanting to override the validation methods. When the default
055 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
056 * convenient way to validate various kinds of objects.</p>
057 *
058 * @author  Martin Desruisseaux (Geomatys)
059 * @version 3.1
060 * @since   2.2
061 */
062public class GeometryValidator extends Validator {
063    /**
064     * Small relative tolerance values for comparisons of floating point numbers.
065     * The default value is {@value org.opengis.test.Validator#DEFAULT_TOLERANCE}.
066     * Implementors can change this value before to run the tests.
067     */
068    public double tolerance = DEFAULT_TOLERANCE;
069
070    /**
071     * Creates a new validator instance.
072     *
073     * @param container  the set of validators to use for validating other kinds of objects
074     *                   (see {@linkplain #container field javadoc}).
075     */
076    public GeometryValidator(final ValidatorContainer container) {
077        super(container, "org.opengis.geometry");
078    }
079
080    /**
081     * Returns {@code true} if the given range is [+0 … -0]. Such range is used by some implementations
082     * for representing a 360° turn around the Earth. Such convention is of course not mandatory, but
083     * some tests in this class must be aware of it.
084     */
085    private static boolean isPositiveToNegativeZero(final double lower, final double upper) {
086        return Double.doubleToRawLongBits(lower) == 0L &&                       // Positive zero
087               Double.doubleToRawLongBits(upper) == Long.MIN_VALUE;             // Negative zero
088    }
089
090    /**
091     * Validates the given envelope.
092     * This method performs the following verifications:
093     *
094     * <ul>
095     *   <li>Envelope and corners dimension shall be the same.</li>
096     *   <li>Envelope and corners CRS shall be the same, ignoring {@code null} values.</li>
097     *   <li>Lower, upper and median ordinate values shall be inside the [minimum … maximum] range.</li>
098     *   <li>Lower &gt; upper ordinate values are allowed only on axis having wraparound range meaning.</li>
099     *   <li>For the usual lower &lt; upper case, compares the minimum, maximum, median and span values
100     *       against values computed from the lower and upper ordinates.</li>
101     * </ul>
102     *
103     * @param  object  the object to validate, or {@code null}.
104     */
105    public void validate(final Envelope object) {
106        if (object == null) {
107            return;
108        }
109        final int dimension = object.getDimension();
110        assertPositive("Envelope: dimension can not be negative.", dimension);
111        final CoordinateReferenceSystem crs = object.getCoordinateReferenceSystem();
112        container.validate(crs);                                                            // May be null.
113        CoordinateSystem cs = null;
114        if (crs != null) {
115            cs = crs.getCoordinateSystem();
116            if (cs != null) {
117                assertEquals("Envelope: CRS dimension shall be equal to the envelope dimension",
118                        dimension, cs.getDimension());
119            }
120        }
121        /*
122         * Validates the corners as DirectPosition objects,
123         * then checks Coordinate Reference Systems and dimensions.
124         */
125        final DirectPosition lowerCorner = object.getLowerCorner();
126        final DirectPosition upperCorner = object.getUpperCorner();
127        mandatory("Envelope: shall have a lower corner.",  lowerCorner);
128        mandatory("Envelope: shall have an upper corner.", upperCorner);
129        validate(lowerCorner);
130        validate(upperCorner);
131        CoordinateReferenceSystem lowerCRS = null;
132        CoordinateReferenceSystem upperCRS = null;
133        if (lowerCorner != null) {
134            lowerCRS = lowerCorner.getCoordinateReferenceSystem();
135            assertEquals("Envelope: lower corner dimension shall be equal to the envelope dimension.",
136                    dimension, lowerCorner.getDimension());
137        }
138        if (upperCorner != null) {
139            upperCRS = upperCorner.getCoordinateReferenceSystem();
140            assertEquals("Envelope: upper corner dimension shall be equal to the envelope dimension.",
141                    dimension, upperCorner.getDimension());
142        }
143        if (crs != null) {
144            if (lowerCRS != null) assertSame("Envelope: lower CRS shall be the same than the envelope CRS.", crs, lowerCRS);
145            if (upperCRS != null) assertSame("Envelope: upper CRS shall be the same than the envelope CRS.", crs, upperCRS);
146        } else if (lowerCRS != null && upperCRS != null) {
147            assertSame("Envelope: the two corners shall have the same CRS.", lowerCRS, upperCRS);
148        }
149        /*
150         * Verifies the consistency of lower, upper, minimum, maximum, median and span values.
151         * The tests are relaxed in the case of ranges spanning the wraparound limit (e.g. the
152         * anti-meridian).
153         */
154        for (int i=0; i<dimension; i++) {
155            RangeMeaning meaning = null;
156            if (cs != null) {
157                final CoordinateSystemAxis axis = cs.getAxis(i);
158                if (axis != null) { // Should never be null, but this is not this test's job to ensure that.
159                    meaning = axis.getRangeMeaning();
160                }
161            }
162            final double lower   = (lowerCorner != null) ? lowerCorner.getOrdinate(i) : NaN;
163            final double upper   = (upperCorner != null) ? upperCorner.getOrdinate(i) : NaN;
164            final double minimum = object.getMinimum(i);
165            final double maximum = object.getMaximum(i);
166            final double median  = object.getMedian (i);
167            final double span    = object.getSpan   (i);
168            if (!isNaN(minimum) && !isNaN(maximum)) {
169                if (lower <= upper && !isPositiveToNegativeZero(lower, upper)) { // Do not accept NaN in this block.
170                    final double eps = (upper - lower) * tolerance;
171                    assertEquals("Envelope: minimum value shall be equal to the lower corner ordinate.", lower, minimum, eps);
172                    assertEquals("Envelope: maximum value shall be equal to the upper corner ordinate.", upper, maximum, eps);
173                    assertEquals("Envelope: unexpected span value.",   (maximum - minimum),   span,   eps);
174                    assertEquals("Envelope: unexpected median value.", (maximum + minimum)/2, median, eps);
175                } else if (RangeMeaning.EXACT.equals(meaning)) {
176                    // assertBetween(…) tolerates NaN values, which is what we want.
177                    assertValidRange("Envelope: invalid minimum or maximum.", minimum, maximum);
178                    assertBetween   ("Envelope: invalid lower ordinate.",     minimum, maximum, lower);
179                    assertBetween   ("Envelope: invalid upper ordinate.",     minimum, maximum, upper);
180                    assertBetween   ("Envelope: invalid median ordinate.",    minimum, maximum, median);
181                }
182            }
183            if (meaning != null && (lower > upper || isPositiveToNegativeZero(lower, upper))) {
184                assertEquals("Envelope: lower ordinate value may be greater than upper ordinate value "
185                        + "only on axis having wrappround range.", RangeMeaning.WRAPAROUND, meaning);
186            }
187        }
188    }
189
190    /**
191     * Validates the given position.
192     * This method ensures that the following hold:
193     *
194     * <ul>
195     *   <li>The number of dimension can not be negative.</li>
196     *   <li>If the position is associated to a CRS, then their number of dimensions must be equal.</li>
197     *   <li>Length of {@link DirectPosition#getCoordinate()} must be equals to the number of dimensions.</li>
198     *   <li>Values of above array must be equals to values returned by {@link DirectPosition#getOrdinate(int)}.</li>
199     *   <li>If the position is associated to a CRS and the axis range meaning is {@link RangeMeaning#EXACT},
200     *       then the ordinate values must be between the minimum and maximum axis value.</li>
201     * </ul>
202     *
203     * @param  object  the object to validate, or {@code null}.
204     */
205    public void validate(final DirectPosition object) {
206        if (object == null) {
207            return;
208        }
209        /*
210         * Checks coordinate consistency.
211         */
212        final int dimension = object.getDimension();
213        assertPositive("DirectPosition: dimension can not be negative.", dimension);
214        final double[] coordinate = object.getCoordinate();
215        mandatory("DirectPosition: coordinate array can not be null.", coordinate);
216        if (coordinate != null) {
217            assertEquals("DirectPosition: coordinate array length shall be equal to the dimension.",
218                    dimension, coordinate.length);
219            for (int i=0; i<dimension; i++) {
220                assertEquals("DirectPosition: getOrdinate(i) shall be the same than coordinate[i].",
221                        coordinate[i], object.getOrdinate(i), 0.0);         // No tolerance - we want exact match.
222            }
223        }
224        /*
225         * Checks coordinate validity in the CRS.
226         */
227        final CoordinateReferenceSystem crs = object.getCoordinateReferenceSystem();
228        container.validate(crs);                                                        // May be null.
229        int hashCode = 0;
230        if (crs != null) {
231            final CoordinateSystem cs = crs.getCoordinateSystem();                      // Assume already validated.
232            if (cs != null) {
233                assertEquals("DirectPosition: CRS dimension must matches the position dimension.",
234                        dimension, cs.getDimension());
235                for (int i=0; i<dimension; i++) {
236                    final CoordinateSystemAxis axis = cs.getAxis(i);                    // Assume already validated.
237                    if (axis != null && RangeMeaning.EXACT.equals(axis.getRangeMeaning())) {
238                        final double ordinate = coordinate[i];
239                        final double minimum  = axis.getMinimumValue();
240                        final double maximum  = axis.getMaximumValue();
241                        assertBetween("DirectPosition: ordinate out of axis bounds.", minimum, maximum, ordinate);
242                    }
243                }
244            }
245            hashCode = crs.hashCode();
246        }
247        /*
248         * Tests hash code values. It must be compliant to DirectPosition.hashCode()
249         * contract stated in the javadoc.
250         */
251        hashCode += Arrays.hashCode(coordinate);
252        assertEquals("DirectPosition: hashCode shall be compliant to the contract given in javadoc.",
253                hashCode, object.hashCode());
254        assertTrue("DirectPosition: shall be equal to itself.", object.equals(object));
255        /*
256         * Ensures that the array returned by DirectPosition.getCoordinate() is a clone.
257         */
258        for (int i=0; i<dimension; i++) {
259            final double oldValue = coordinate[i];
260            coordinate[i] *= 2;
261            assertEquals("DirectPosition: coordinate array shall be cloned.",
262                    oldValue, object.getOrdinate(i), 0.0);                      // No tolerance - we want exact match.
263        }
264    }
265}