001/* 002 * GeoAPI - Java interfaces for OGC/ISO standards 003 * Copyright © 2015-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.referencing; 019 020import java.time.Instant; 021import java.time.OffsetDateTime; 022import java.time.temporal.Temporal; 023import java.time.chrono.ChronoZonedDateTime; 024import javax.measure.Unit; 025import javax.measure.UnitConverter; 026import javax.measure.IncommensurableException; 027import javax.measure.quantity.Angle; 028import javax.measure.quantity.Length; 029import org.opengis.metadata.Identifier; 030import org.opengis.metadata.extent.Extent; 031import org.opengis.metadata.extent.GeographicBoundingBox; 032import org.opengis.metadata.extent.GeographicDescription; 033import org.opengis.metadata.extent.GeographicExtent; 034import org.opengis.metadata.extent.TemporalExtent; 035import org.opengis.metadata.extent.VerticalExtent; 036import org.opengis.parameter.ParameterValue; 037import org.opengis.parameter.ParameterValueGroup; 038import org.opengis.referencing.ObjectDomain; 039import org.opengis.referencing.IdentifiedObject; 040import org.opengis.referencing.datum.Ellipsoid; 041import org.opengis.referencing.datum.PrimeMeridian; 042import org.opengis.referencing.datum.GeodeticDatum; 043import org.opengis.referencing.cs.AxisDirection; 044import org.opengis.referencing.cs.CoordinateSystem; 045import org.opengis.referencing.cs.CoordinateSystemAxis; 046import org.opengis.referencing.crs.CoordinateReferenceSystem; 047import org.opengis.referencing.crs.VerticalCRS; 048import org.opengis.referencing.cs.VerticalCS; 049import org.opengis.temporal.Period; 050import org.opengis.temporal.TemporalPrimitive; 051import org.opengis.test.TestCase; 052import org.opentest4j.AssertionFailedError; 053 054import static java.lang.Double.isNaN; 055import static org.junit.jupiter.api.Assertions.*; 056import static org.opengis.test.Assertions.assertUnicodeIdentifierEquals; 057 058 059/** 060 * Base class of {@link CoordinateReferenceSystem} implementation tests. 061 * This base class provides {@code verify(…)} methods that subclasses can override if they need to alter 062 * the object verifications. 063 * 064 * @author Martin Desruisseaux (Geomatys) 065 * @version 3.1 066 * @since 3.1 067 */ 068@SuppressWarnings("strictfp") // Because we still target Java 11. 069public strictfp abstract class ReferencingTestCase extends TestCase { 070 /** 071 * Creates a test case initialized to default values. 072 */ 073 protected ReferencingTestCase() { 074 } 075 076 /** 077 * Returns the given wrapper as a primitive value, or NaN if null. 078 * 079 * @param value the value to unwrap. 080 * @return the unwrapped value. 081 */ 082 private static double toPrimitive(final Double value) { 083 return (value != null) ? value : Double.NaN; 084 } 085 086 /** 087 * Converts the given date to Julian days. 088 * 089 * @param time the date to convert. 090 * @return the Julian days for the given date. 091 */ 092 private static double julian(final Instant time) { 093 return (time.getEpochSecond() - (-2440588L * (24*60*60) + (12*60*60))) / (24*60*60.0); 094 } 095 096 /** 097 * Infers a value from the extent as an instant and computes the union with a lower or upper bounds. 098 * 099 * @param bound the current lower ({@code begin == true}) or upper ({@code begin == false}) bound. 100 * @param extent the extent from which to read a bound. 101 * @param begin {@code true} for the start time, or {@code false} for the end time. 102 * @return the new bound value. 103 */ 104 private static Instant union(final Instant bound, final TemporalPrimitive extent, final boolean begin) { 105 if (extent instanceof Period) { 106 final var period = (Period) extent; 107 final Temporal date = (begin ? period.getBeginning() : period.getEnding()).getPosition(); 108 final Instant instant; 109 if (date instanceof Instant) { 110 instant = (Instant) date; 111 } else if (date instanceof OffsetDateTime) { 112 instant = ((OffsetDateTime) date).toInstant(); 113 } else if (date instanceof ChronoZonedDateTime) { 114 instant = ((ChronoZonedDateTime) date).toInstant(); 115 } else { 116 return bound; 117 } 118 if (instant != null && (bound == null || (begin ? instant.isBefore(bound) : instant.isAfter(bound)))) { 119 return instant; 120 } 121 } 122 return bound; 123 } 124 125 /** 126 * Compares the name and identifier of the given {@code object} against the expected values. 127 * This method allows for some flexibilities: 128 * 129 * <ul> 130 * <li>For {@link IdentifiedObject#getName()}: 131 * <ul> 132 * <li>Only the value returned by {@link Identifier#getCode()} is verified. 133 * The code space, authority and version are ignored.</li> 134 * <li>Only the characters that are valid for Unicode identifiers are compared (ignoring case), as documented in 135 * {@link org.opengis.test.Assertions#assertUnicodeIdentifierEquals Assertions.assertUnicodeIdentifierEquals(…)}.</li> 136 * </ul> 137 * </li> 138 * <li>For {@link IdentifiedObject#getIdentifiers()}: 139 * <ul> 140 * <li>Only the value returned by {@link Identifier#getCode()} is verified. 141 * The code space, authority and version are ignored.</li> 142 * <li>The identifiers collection can contain more identifiers than the expected one, 143 * and the expected identifier does not need to be first.</li> 144 * <li>The comparison is case-insensitive.</li> 145 * </ul> 146 * </li> 147 * </ul> 148 * 149 * If the given {@code object} is {@code null}, then this method does nothing. 150 * Deciding if {@code null} objects are allowed or not is {@link org.opengis.test.Validator}'s job. 151 * 152 * @param object the object to verify, or {@code null} if none. 153 * @param name the expected name (ignoring code space), or {@code null} if unrestricted. 154 * @param identifier the expected identifier code (ignoring code space), or {@code null} if unrestricted. 155 */ 156 protected void verifyIdentification(final IdentifiedObject object, final String name, final String identifier) { 157 if (object != null) { 158 if (name != null) { 159 assertUnicodeIdentifierEquals(name, Utilities.getName(object), true, "getName().getCode()"); 160 } 161 if (identifier != null) { 162 for (final Identifier id : object.getIdentifiers()) { 163 assertNotNull(id, "getName().getIdentifiers()"); 164 if (identifier.equalsIgnoreCase(id.getCode())) { 165 return; 166 } 167 } 168 fail("getName().getIdentifiers(): element “" + identifier + "” not found."); 169 } 170 } 171 } 172 173 /** 174 * Compares the name, axis lengths and inverse flattening factor of the given ellipsoid against the expected values. 175 * This method allows for some flexibilities: 176 * 177 * <ul> 178 * <li>{@link Ellipsoid#getName()} allows for the same flexibilities as the one documented in 179 * {@link #verifyIdentification verifyIdentification(…)}.</li> 180 * <li>{@link Ellipsoid#getSemiMajorAxis()} does not need to use the unit of measurement given 181 * by the {@code axisUnit} argument. Unit conversion will be applied as needed.</li> 182 * </ul> 183 * 184 * The tolerance thresholds are 0.5 unit of the last digits of the values found in the EPSG database: 185 * <ul> 186 * <li>3 decimal digits for {@code semiMajor} values in metres.</li> 187 * <li>9 decimal digits for {@code inverseFlattening} values.</li> 188 * </ul> 189 * 190 * If the given {@code ellipsoid} is {@code null}, then this method does nothing. 191 * Deciding if {@code null} datum are allowed or not is {@link org.opengis.test.Validator}'s job. 192 * 193 * @param ellipsoid the ellipsoid to verify, or {@code null} if none. 194 * @param name the expected name (ignoring code space), or {@code null} if unrestricted. 195 * @param semiMajor the expected semi-major axis length, in units given by the {@code axisUnit} argument. 196 * @param inverseFlattening the expected inverse flattening factor. 197 * @param axisUnit the unit of the {@code semiMajor} argument (not necessarily the actual unit of the ellipsoid). 198 * 199 * @see GeodeticDatum#getEllipsoid() 200 */ 201 protected void verifyFlattenedSphere(final Ellipsoid ellipsoid, final String name, 202 final double semiMajor, final double inverseFlattening, final Unit<Length> axisUnit) 203 { 204 if (ellipsoid != null) { 205 if (name != null) { 206 assertUnicodeIdentifierEquals(name, Utilities.getName(ellipsoid), true, 207 "Ellipsoid.getName().getCode()"); 208 } 209 final Unit<Length> actualUnit = ellipsoid.getAxisUnit(); 210 assertNotNull(actualUnit, "Ellipsoid.getAxisUnit()"); 211 assertEquals(semiMajor, 212 actualUnit.getConverterTo(axisUnit).convert(ellipsoid.getSemiMajorAxis()), 213 units.metre().getConverterTo(axisUnit).convert(5E-4), 214 "Ellipsoid.getSemiMajorAxis()"); 215 assertEquals(inverseFlattening, ellipsoid.getInverseFlattening(), 5E-10, 216 "Ellipsoid.getInverseFlattening()"); 217 } 218 } 219 220 /** 221 * Compares the name and Greenwich longitude of the given prime meridian against the expected values. 222 * This method allows for some flexibilities: 223 * 224 * <ul> 225 * <li>{@link PrimeMeridian#getName()} allows for the same flexibilities as the one documented in 226 * {@link #verifyIdentification verifyIdentification(…)}.</li> 227 * <li>{@link PrimeMeridian#getGreenwichLongitude()} does not need to use the unit of measurement given 228 * by the {@code angularUnit} argument. Unit conversion will be applied as needed.</li> 229 * </ul> 230 * 231 * The tolerance threshold is 0.5 unit of the last digit of the values found in the EPSG database: 232 * <ul> 233 * <li>7 decimal digits for {@code greenwichLongitude} values in degrees.</li> 234 * </ul> 235 * 236 * If the given {@code primeMeridian} is {@code null}, then this method does nothing. 237 * Deciding if {@code null} prime meridians are allowed or not is {@link org.opengis.test.Validator}'s job. 238 * 239 * @param primeMeridian the prime meridian to verify, or {@code null} if none. 240 * @param name the expected name (ignoring code space), or {@code null} if unrestricted. 241 * @param greenwichLongitude the expected Greenwich longitude, in units given by the {@code angularUnit} argument. 242 * @param angularUnit the unit of the {@code greenwichLongitude} argument (not necessarily the actual unit of the prime meridian). 243 * 244 * @see GeodeticDatum#getPrimeMeridian() 245 */ 246 protected void verifyPrimeMeridian(final PrimeMeridian primeMeridian, final String name, 247 final double greenwichLongitude, final Unit<Angle> angularUnit) 248 { 249 if (primeMeridian != null) { 250 if (name != null) { 251 assertUnicodeIdentifierEquals(name, Utilities.getName(primeMeridian), true, 252 "PrimeMeridian.getName().getCode()"); 253 } 254 final Unit<Angle> actualUnit = primeMeridian.getAngularUnit(); 255 assertNotNull(actualUnit, "PrimeMeridian.getAngularUnit()"); 256 assertEquals(greenwichLongitude, 257 actualUnit.getConverterTo(angularUnit).convert(primeMeridian.getGreenwichLongitude()), 258 units.degree().getConverterTo(angularUnit).convert(5E-8), 259 "PrimeMeridian.getGreenwichLongitude()"); 260 } 261 } 262 263 /** 264 * Compares the type, axis units and directions of the given coordinate system against the expected values. 265 * This method does not verify the coordinate system name because it is usually not significant. 266 * This method does not verify axis names neither because the names specified by ISO 19111 and ISO 19162 differ. 267 * 268 * <p>If the given {@code cs} is {@code null}, then this method does nothing. 269 * Deciding if {@code null} coordinate systems are allowed or not is {@link org.opengis.test.Validator}'s job.</p> 270 * 271 * @param cs the coordinate system to verify, or {@code null} if none. 272 * @param type the expected coordinate system type. 273 * @param directions the expected axis directions. The length of this array determines the expected {@code cs} dimension. 274 * @param axisUnits the expected axis units. If the array length is less than the {@code cs} dimension, 275 * then the last unit is repeated for all remaining dimensions. 276 * If the array length is greater, than extra units are ignored. 277 * 278 * @see CoordinateReferenceSystem#getCoordinateSystem() 279 */ 280 protected void verifyCoordinateSystem(final CoordinateSystem cs, final Class<? extends CoordinateSystem> type, 281 final AxisDirection[] directions, final Unit<?>... axisUnits) 282 { 283 if (cs != null) { 284 assertEquals(directions.length, cs.getDimension(), "CoordinateSystem.getDimension()"); 285 for (int i=0; i<directions.length; i++) { 286 final CoordinateSystemAxis axis = cs.getAxis(i); 287 assertNotNull(axis, "CoordinateSystem.getAxis(*)"); 288 assertEquals(directions[i], axis.getDirection(), "CoordinateSystem.getAxis(*).getDirection()"); 289 assertEquals(axisUnits[Math.min(i, axisUnits.length-1)], axis.getUnit(), "CoordinateSystem.getAxis(*).getUnit()"); 290 } 291 } 292 } 293 294 /** 295 * Compares an operation parameter against the expected value. 296 * This method allows for some flexibilities: 297 * 298 * <ul> 299 * <li>The parameter does not need to use the unit of measurement given by the {@code unit} argument. 300 * Unit conversion should be applied as needed by the {@link ParameterValue#doubleValue(Unit)} method.</li> 301 * </ul> 302 * 303 * If the given {@code group} is {@code null}, then this method does nothing. 304 * Deciding if {@code null} parameters are allowed or not is {@link org.opengis.test.Validator}'s job. 305 * 306 * @param group the parameter group containing the parameter to test. 307 * @param name the name of the parameter to test. 308 * @param value the expected parameter value when expressed in units given by the {@code unit} argument. 309 * @param unit the units of measurement of the {@code value} argument 310 * (not necessarily the unit actually used by the implementation). 311 */ 312 protected void verifyParameter(final ParameterValueGroup group, final String name, final double value, final Unit<?> unit) { 313 if (group != null) { 314 final ParameterValue<?> param = group.parameter(name); 315 assertNotNull(param, name); 316 assertEquals(param.doubleValue(unit), value, StrictMath.abs(value * 1E-10), name); 317 } 318 } 319 320 /** 321 * Compares the geographic description and bounding box of the given extent against the expected values. 322 * This method allows for some flexibilities: 323 * 324 * <ul> 325 * <li>For {@link GeographicDescription} elements: 326 * <ul> 327 * <li>Descriptions are considered optional. If the given {@code extent} does not contain any 328 * {@code GeographicDescription} element, then the given {@code description} argument is ignored.</li> 329 * <li>If the given {@code extent} contains more than one {@code GeographicDescription} element, then only 330 * one of them (not necessarily the first one) needs to have the given {@code description} value. 331 * Other elements are ignored.</li> 332 * </ul> 333 * </li> 334 * <li>For {@link GeographicBoundingBox} elements: 335 * <ul> 336 * <li>Bounding boxes are considered optional. If the given {@code extent} does not contain any 337 * {@code GeographicBoundingBox} element, then all given bound arguments are ignored.</li> 338 * <li>If the given {@code extent} contains more than one {@code GeographicBoundingBox} element, 339 * then the union of them is compared against the given bound arguments.</li> 340 * </ul> 341 * </li> 342 * </ul> 343 * 344 * The tolerance threshold is 0.005° since geographic bounding box are only approximate information. 345 * 346 * <p>If the given {@code extent} is {@code null}, then this method does nothing. 347 * Deciding if {@code null} extents are allowed or not is {@link org.opengis.test.Validator}'s job.</p> 348 * 349 * @param extent the extent to verify, or {@code null} if none. 350 * @param description the expected area, or {@code null} if unrestricted. 351 * @param southBoundLatitude the expected minimum latitude, or NaN if unrestricted. 352 * @param westBoundLongitude the expected minimum longitude, or NaN if unrestricted. 353 * @param northBoundLatitude the expected maximum latitude, or NaN if unrestricted. 354 * @param eastBoundLongitude the expected maximum longitude, or NaN if unrestricted. 355 * 356 * @see ObjectDomain#getDomainOfValidity() 357 */ 358 protected void verifyGeographicExtent(final Extent extent, final String description, 359 final double southBoundLatitude, final double westBoundLongitude, 360 final double northBoundLatitude, final double eastBoundLongitude) 361 { 362 if (extent != null) { 363 double ymin = Double.POSITIVE_INFINITY; 364 double xmin = Double.POSITIVE_INFINITY; 365 double ymax = Double.NEGATIVE_INFINITY; 366 double xmax = Double.NEGATIVE_INFINITY; 367 String unknownArea = null; 368 for (final GeographicExtent e : extent.getGeographicElements()) { 369 if (e instanceof GeographicBoundingBox) { 370 final GeographicBoundingBox bbox = (GeographicBoundingBox) e; 371 double t; 372 if ((t = bbox.getSouthBoundLatitude()) < ymin) ymin = t; 373 if ((t = bbox.getWestBoundLongitude()) < xmin) xmin = t; 374 if ((t = bbox.getNorthBoundLatitude()) > ymax) ymax = t; 375 if ((t = bbox.getEastBoundLongitude()) > xmax) xmax = t; 376 } 377 /* 378 * Description: optional, but if present we allow any number of identifiers 379 * provided that at least one contain the expected string. 380 */ 381 if (description != null && e instanceof GeographicDescription) { 382 final String area = ((GeographicDescription) e).getGeographicIdentifier().getCode(); 383 if (description.equals(area)) { 384 unknownArea = null; 385 break; 386 } 387 if (unknownArea == null) { 388 unknownArea = area; // For reporting an error message if we do not find the expected area. 389 } 390 } 391 } 392 if (unknownArea != null) { 393 assertEquals(description, unknownArea, "GeographicDescription"); 394 } 395 /* 396 * WKT 2 specification said that BBOX precision should be about 0.01°. 397 */ 398 if (!isNaN(southBoundLatitude) && ymin != Double.POSITIVE_INFINITY) assertEquals(southBoundLatitude, ymin, 0.005, "getSouthBoundLatitude()"); 399 if (!isNaN(westBoundLongitude) && xmin != Double.POSITIVE_INFINITY) assertEquals(westBoundLongitude, xmin, 0.005, "getWestBoundLongitude()"); 400 if (!isNaN(northBoundLatitude) && ymax != Double.NEGATIVE_INFINITY) assertEquals(northBoundLatitude, ymax, 0.005, "getNorthBoundLatitude()"); 401 if (!isNaN(eastBoundLongitude) && xmax != Double.NEGATIVE_INFINITY) assertEquals(eastBoundLongitude, xmax, 0.005, "getEastBoundLongitude()"); 402 } 403 } 404 405 /** 406 * Compares the vertical elements of the given extent against the expected values. 407 * This method allows for some flexibilities: 408 * 409 * <ul> 410 * <li>Vertical extents are considered optional. If the given {@code extent} does not contain any 411 * {@code VerticalExtent} element, then this method does nothing.</li> 412 * <li>{@link VerticalExtent#getMinimumValue()} and {@link VerticalExtent#getMaximumValue() getMaximumValue()} 413 * do not need to use the unit of measurement given by the {@code unit} argument. Unit conversions will be 414 * applied as needed if {@link VerticalExtent#getVerticalCRS()} returns a non-null value.</li> 415 * <li>If the given {@code extent} contains more than one {@code VerticalExtent} element, 416 * then the union of them is compared against the given bound arguments.</li> 417 * </ul> 418 * 419 * <p>If the given {@code extent} is {@code null}, then this method does nothing. 420 * Deciding if {@code null} extents are allowed or not is {@link org.opengis.test.Validator}'s job.</p> 421 * 422 * @param extent the extent to verify, or {@code null} if none. 423 * @param minimumValue the expected minimal vertical value, or NaN if unrestricted. 424 * @param maximumValue the expected maximal vertical value, or NaN if unrestricted. 425 * @param tolerance the tolerance threshold to use for comparison. 426 * @param unit the unit of {@code minimumValue}, {@code maximumValue} and {@code tolerance} arguments, 427 * or {@code null} for skipping the unit conversion. 428 * 429 * @see ObjectDomain#getDomainOfValidity() 430 */ 431 protected void verifyVerticalExtent(final Extent extent, 432 final double minimumValue, final double maximumValue, final double tolerance, final Unit<?> unit) 433 { 434 if (extent != null) { 435 double min = Double.POSITIVE_INFINITY; 436 double max = Double.NEGATIVE_INFINITY; 437 for (final VerticalExtent e : extent.getVerticalElements()) { 438 double minValue = toPrimitive(e.getMinimumValue()); 439 double maxValue = toPrimitive(e.getMaximumValue()); 440 if (unit != null) { 441 final VerticalCRS crs = e.getVerticalCRS(); 442 if (crs != null) { 443 final VerticalCS cs = crs.getCoordinateSystem(); 444 if (cs != null) { 445 assertEquals(1, cs.getDimension(), 446 "VerticalExtent.getVerticalCRS().getCoordinateSystem().getDimension()"); 447 final CoordinateSystemAxis axis = cs.getAxis(0); 448 if (axis != null) { 449 final Unit<?> u = axis.getUnit(); 450 if (u != null) { 451 final UnitConverter c; 452 try { 453 c = u.getConverterToAny(unit); 454 } catch (IncommensurableException ex) { 455 throw new AssertionFailedError("Expected VerticalExtent in units of “" 456 + unit + "” but got units of “" + u + "”.", ex); 457 } 458 minValue = c.convert(minValue); 459 maxValue = c.convert(maxValue); 460 } 461 } 462 } 463 } 464 } 465 if (minValue < min) min = minValue; 466 if (maxValue > max) max = maxValue; 467 } 468 if (!isNaN(minimumValue) && min != Double.POSITIVE_INFINITY) assertEquals(minimumValue, min, tolerance, "VerticalExtent.getMinimumValue()"); 469 if (!isNaN(maximumValue) && max != Double.NEGATIVE_INFINITY) assertEquals(maximumValue, max, tolerance, "VerticalExtent.getMaximumValue()"); 470 } 471 } 472 473 /** 474 * Compares the temporal elements of the given extent against the expected values. 475 * This method allows for some flexibilities: 476 * 477 * <ul> 478 * <li>Temporal extents are considered optional. If the given {@code extent} does not contain any 479 * {@code TemporalExtent} element, or if extent bounds can not be represented as {@link Instant} 480 * objects, then this method does nothing.</li> 481 * <li>If the given {@code extent} contains more than one {@code TemporalExtent} element, 482 * then the union of them is compared against the given bound arguments.</li> 483 * </ul> 484 * 485 * If the given {@code extent} is {@code null}, then this method does nothing. 486 * Deciding if {@code null} extents are allowed or not is {@link org.opengis.test.Validator}'s job. 487 * 488 * @param extent the extent to verify, or {@code null} if none. 489 * @param startTime the expected start time, or {@code null} if unrestricted. 490 * @param endTime the expected end time, or {@code null} if unrestricted. 491 * @param tolerance the tolerance threshold to use for comparison, in unit of days. 492 * 493 * @see ObjectDomain#getDomainOfValidity() 494 */ 495 protected void verifyTimeExtent(final Extent extent, final Instant startTime, final Instant endTime, final double tolerance) { 496 if (extent != null) { 497 Instant min = null; 498 Instant max = null; 499 for (final TemporalExtent e : extent.getTemporalElements()) { 500 final TemporalPrimitive p = e.getExtent(); 501 min = union(min, p, true); 502 max = union(max, p, false); 503 } 504 if (startTime != null && min != null) { 505 assertEquals(julian(startTime), julian(min), tolerance, "TemporalExtent start time (julian days)"); 506 } 507 if (endTime != null && max != null) { 508 assertEquals(julian(endTime), julian(max), tolerance, "TemporalExtent end time (julian days)"); 509 } 510 } 511 } 512}