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 > upper coordinate values are allowed only on axis having wraparound range meaning.</li> 092 * <li>For the usual lower < 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}