001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2008-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.geometry;
019
020import java.util.Arrays;
021
022import org.opengis.geometry.*;
023import org.opengis.referencing.cs.CoordinateSystem;
024import org.opengis.referencing.cs.CoordinateSystemAxis;
025import org.opengis.referencing.crs.CoordinateReferenceSystem;
026import org.opengis.referencing.cs.RangeMeaning;
027
028import org.opengis.test.Validator;
029import org.opengis.test.ValidatorContainer;
030
031import static java.lang.Double.NaN;
032import static java.lang.Double.isNaN;
033import static org.junit.jupiter.api.Assertions.*;
034import static org.opengis.test.Assertions.assertBetween;
035import static org.opengis.test.Assertions.assertPositive;
036import static org.opengis.test.Assertions.assertValidRange;
037
038
039/**
040 * Validates {@link Geometry} and related objects from the {@code org.opengis.geometry}
041 * package.
042 *
043 * <p>This class is provided for users wanting to override the validation methods. When the default
044 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
045 * convenient way to validate various kinds of objects.</p>
046 *
047 * @author  Martin Desruisseaux (Geomatys)
048 * @version 3.1
049 * @since   2.2
050 */
051public class GeometryValidator extends Validator {
052    /**
053     * Small relative tolerance values for comparisons of floating point numbers.
054     * The default value is {@value org.opengis.test.Validator#DEFAULT_TOLERANCE}.
055     * Implementers can change this value before to run the tests.
056     */
057    public double tolerance = DEFAULT_TOLERANCE;
058
059    /**
060     * Creates a new validator instance.
061     *
062     * @param container  the set of validators to use for validating other kinds of objects
063     *                   (see {@linkplain #container field javadoc}).
064     */
065    public GeometryValidator(final ValidatorContainer container) {
066        super(container, "org.opengis.geometry");
067    }
068
069    /**
070     * Returns {@code true} if the given range is [+0 … -0]. Such range is used by some implementations
071     * for representing a 360° turn around the Earth. Such convention is of course not mandatory, but
072     * some tests in this class must be aware of it.
073     *
074     * @param  lower  the lower value of the range to test.
075     * @param  upper  the upper value of the range to test.
076     * @return whether the given range is [+0 … -0].
077     */
078    private static boolean isPositiveToNegativeZero(final double lower, final double upper) {
079        return Double.doubleToRawLongBits(lower) == 0L &&                       // Positive zero
080               Double.doubleToRawLongBits(upper) == Long.MIN_VALUE;             // Negative zero
081    }
082
083    /**
084     * Validates the given envelope.
085     * This method performs the following verifications:
086     *
087     * <ul>
088     *   <li>Envelope and corners dimension shall be the same.</li>
089     *   <li>Envelope and corners CRS shall be the same, ignoring {@code null} values.</li>
090     *   <li>Lower, upper and median coordinate values shall be inside the [minimum … maximum] range.</li>
091     *   <li>Lower &gt; upper coordinate values are allowed only on axis having wraparound range meaning.</li>
092     *   <li>For the usual lower &lt; upper case, compares the minimum, maximum, median and span values
093     *       against values computed from the lower and upper coordinates.</li>
094     * </ul>
095     *
096     * @param  object  the object to validate, or {@code null}.
097     */
098    public void validate(final Envelope object) {
099        if (object == null) {
100            return;
101        }
102        final int dimension = object.getDimension();
103        assertPositive(dimension, "Envelope: dimension cannot be negative.");
104        final CoordinateReferenceSystem crs = object.getCoordinateReferenceSystem();
105        container.validate(crs);                                                            // May be null.
106        CoordinateSystem cs = null;
107        if (crs != null) {
108            cs = crs.getCoordinateSystem();
109            if (cs != null) {
110                assertEquals(dimension, cs.getDimension(),
111                        "Envelope: CRS dimension shall be equal to the envelope dimension");
112            }
113        }
114        /*
115         * Validates the corners as DirectPosition objects,
116         * then checks Coordinate Reference Systems and dimensions.
117         */
118        final DirectPosition lowerCorner = object.getLowerCorner();
119        final DirectPosition upperCorner = object.getUpperCorner();
120        mandatory(lowerCorner, "Envelope: shall have a lower corner.");
121        mandatory(upperCorner, "Envelope: shall have an upper corner.");
122        validate(lowerCorner);
123        validate(upperCorner);
124        CoordinateReferenceSystem lowerCRS = null;
125        CoordinateReferenceSystem upperCRS = null;
126        if (lowerCorner != null) {
127            lowerCRS = lowerCorner.getCoordinateReferenceSystem();
128            assertEquals(dimension, lowerCorner.getDimension(),
129                    "Envelope: lower corner dimension shall be equal to the envelope dimension.");
130        }
131        if (upperCorner != null) {
132            upperCRS = upperCorner.getCoordinateReferenceSystem();
133            assertEquals(dimension, upperCorner.getDimension(),
134                    "Envelope: upper corner dimension shall be equal to the envelope dimension.");
135        }
136        if (crs != null) {
137            if (lowerCRS != null) assertSame(crs, lowerCRS, "Envelope: lower CRS shall be the same as the envelope CRS.");
138            if (upperCRS != null) assertSame(crs, upperCRS, "Envelope: upper CRS shall be the same as the envelope CRS.");
139        } else if (lowerCRS != null && upperCRS != null) {
140            assertSame(lowerCRS, upperCRS, "Envelope: the two corners shall have the same CRS.");
141        }
142        /*
143         * Verifies the consistency of lower, upper, minimum, maximum, median and span values.
144         * The tests are relaxed in the case of ranges spanning the wraparound limit (e.g. the
145         * anti-meridian).
146         */
147        for (int i=0; i<dimension; i++) {
148            RangeMeaning meaning = null;
149            if (cs != null) {
150                final CoordinateSystemAxis axis = cs.getAxis(i);
151                if (axis != null) {         // Should never be null, but it is not this test's job to ensure that.
152                    meaning = axis.getRangeMeaning();
153                }
154            }
155            final double lower   = (lowerCorner != null) ? lowerCorner.getCoordinate(i) : NaN;
156            final double upper   = (upperCorner != null) ? upperCorner.getCoordinate(i) : NaN;
157            final double minimum = object.getMinimum(i);
158            final double maximum = object.getMaximum(i);
159            final double median  = object.getMedian (i);
160            final double span    = object.getSpan   (i);
161            if (!isNaN(minimum) && !isNaN(maximum)) {
162                if (lower <= upper && !isPositiveToNegativeZero(lower, upper)) { // Do not accept NaN in this block.
163                    final double eps = (upper - lower) * tolerance;
164                    assertEquals(lower, minimum, eps, "Envelope: minimum value shall be equal to the lower corner coordinate.");
165                    assertEquals(upper, maximum, eps, "Envelope: maximum value shall be equal to the upper corner coordinate.");
166                    assertEquals((maximum - minimum),   span,   eps, "Envelope: unexpected span value.");
167                    assertEquals((maximum + minimum)/2, median, eps, "Envelope: unexpected median value.");
168                } else if (RangeMeaning.EXACT.equals(meaning)) {
169                    // assertBetween(…) tolerates NaN values, which is what we want.
170                    assertValidRange(minimum, maximum,         "Envelope: invalid minimum or maximum.");
171                    assertBetween   (minimum, maximum, lower,  "Envelope: invalid lower coordinate.");
172                    assertBetween   (minimum, maximum, upper,  "Envelope: invalid upper coordinate.");
173                    assertBetween   (minimum, maximum, median, "Envelope: invalid median coordinate.");
174                }
175            }
176            if (meaning != null && (lower > upper || isPositiveToNegativeZero(lower, upper))) {
177                assertEquals(RangeMeaning.WRAPAROUND, meaning, "Envelope: lower coordinate value may be "
178                        + "greater than upper coordinate value only on axis having wrappround range.");
179            }
180        }
181    }
182
183    /**
184     * Validates the given position.
185     * This method ensures that the following hold:
186     *
187     * <ul>
188     *   <li>The number of dimension cannot be negative.</li>
189     *   <li>If the position is associated to a CRS, then their number of dimensions must be equal.</li>
190     *   <li>Length of {@link DirectPosition#getCoordinates()} must be equals to the number of dimensions.</li>
191     *   <li>Values of above array must be equals to values returned by {@link DirectPosition#getCoordinate(int)}.</li>
192     *   <li>If the position is associated to a CRS and the axis range meaning is {@link RangeMeaning#EXACT},
193     *       then the coordinate values must be between the minimum and maximum axis value.</li>
194     * </ul>
195     *
196     * @param  object  the object to validate, or {@code null}.
197     */
198    public void validate(final DirectPosition object) {
199        if (object == null) {
200            return;
201        }
202        /*
203         * Checks coordinate consistency.
204         */
205        final int dimension = object.getDimension();
206        assertPositive(dimension, "DirectPosition: dimension cannot be negative.");
207        final double[] coordinates = object.getCoordinates();
208        mandatory(coordinates, "DirectPosition: coordinate array cannot be null.");
209        if (coordinates != null) {
210            assertEquals(dimension, coordinates.length,
211                    "DirectPosition: coordinate array length shall be equal to the dimension.");
212            for (int i=0; i<dimension; i++) {
213                assertEquals(coordinates[i], object.getCoordinate(i),   // No tolerance - we want exact match.
214                        "DirectPosition: getCoordinate(i) shall be the same as coordinates[i].");
215            }
216        }
217        /*
218         * Checks coordinate validity in the CRS.
219         */
220        final CoordinateReferenceSystem crs = object.getCoordinateReferenceSystem();
221        container.validate(crs);                                                        // May be null.
222        int hashCode = 0;
223        if (crs != null) {
224            final CoordinateSystem cs = crs.getCoordinateSystem();                      // Assume already validated.
225            if (cs != null) {
226                assertEquals(dimension, cs.getDimension(),
227                        "DirectPosition: CRS dimension must matches the position dimension.");
228                for (int i=0; i<dimension; i++) {
229                    final CoordinateSystemAxis axis = cs.getAxis(i);                    // Assume already validated.
230                    if (axis != null && RangeMeaning.EXACT.equals(axis.getRangeMeaning())) {
231                        final double coordinate = coordinates[i];
232                        final double minimum  = axis.getMinimumValue();
233                        final double maximum  = axis.getMaximumValue();
234                        assertBetween(minimum, maximum, coordinate, "DirectPosition: coordinate out of axis bounds.");
235                    }
236                }
237            }
238            hashCode = crs.hashCode();
239        }
240        /*
241         * Tests hash code values. It must be compliant to DirectPosition.hashCode()
242         * contract stated in the javadoc.
243         */
244        hashCode += Arrays.hashCode(coordinates);
245        assertEquals(hashCode, object.hashCode(),
246                "DirectPosition: hashCode shall be compliant to the contract given in javadoc.");
247        assertTrue(object.equals(object), "DirectPosition: shall be equal to itself.");
248        /*
249         * Ensures that the array returned by DirectPosition.getCoordinates() is a clone.
250         */
251        for (int i=0; i<dimension; i++) {
252            final double oldValue = coordinates[i];
253            coordinates[i] *= 2;
254            assertEquals(oldValue, object.getCoordinate(i),                    // No tolerance - we want exact match.
255                    "DirectPosition: coordinates array shall be cloned.");
256        }
257    }
258}