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.referencing; 019 020import java.util.Set; 021import java.util.List; 022import java.util.ArrayList; 023import java.util.LinkedHashSet; 024import java.util.function.Supplier; 025 026import org.opengis.referencing.cs.*; 027import org.opengis.referencing.crs.*; 028import org.opengis.referencing.datum.*; 029import org.opengis.referencing.operation.Conversion; 030import org.opengis.test.ValidatorContainer; 031 032import static org.junit.jupiter.api.Assertions.*; 033import static org.opengis.test.referencing.Utilities.getName; 034import static org.opengis.test.referencing.Utilities.getAxisDirections; 035 036 037/** 038 * Validates {@link CoordinateReferenceSystem} and related objects from the 039 * {@code org.opengis.referencing.crs} package. 040 * 041 * <p>This class is provided for users wanting to override the validation methods. When the default 042 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more 043 * convenient way to validate various kinds of objects.</p> 044 * 045 * @author Martin Desruisseaux (Geomatys) 046 * @version 3.1 047 * @since 2.2 048 */ 049public class CRSValidator extends ReferencingValidator { 050 /** 051 * The axis names mandated by ISO 19111 for some particular kind of CRS. 052 * See ISO 19111:2019 table 16 at page 27. 053 */ 054 private static final String[] 055 GEOCENTRIC_AXIS_NAME = {"geocentric X", "geocentric Y", "geocentric Z"}, 056 GEOGRAPHIC_AXIS_NAME = {"geodetic latitude", "geodetic longitude", "ellipsoidal height"}, 057 PROJECTED_AXIS_NAME = {"northing", "southing", "easting", "westing", "ellipsoidal height"}, 058 SPHERICAL_AXIS_NAME = {"spherical latitude", "geocentric latitude", "geocentric co-latitude", 059 "spherical longitude", "geodetic longitude", "geocentric radius"}, 060 VERTICAL_AXIS_NAME = {"depth", "gravity-related height", "gravity-related depth"}; 061 /* 062 * Note: the ISO table does not mention "gravity-related depth" as a standard name. 063 * However, this name is used in the EPSG database and seems a natural complement 064 * to the "gravity-related height" standard name. 065 */ 066 067 /** 068 * {@code true} if validation of the conversion by {@link #validateGeneralDerivedCRS} is under way. 069 * Used in order to avoid never-ending recursivity. 070 * 071 * @todo Replace by a more general mechanism in {@link ValidatorContainer}. 072 */ 073 private final ThreadLocal<Boolean> VALIDATING = new ThreadLocal<>(); 074 075 /** 076 * {@code true} if standard names shall be enforced when such names are defined by an OGC/ISO 077 * standard. For example, the ISO 19111 standard constraints the {@link GeographicCRS} axis names 078 * to <q>geodetic latitude</q>, <q>geodetic longitude</q> and <q>ellipsoidal height</q> (if 3D) names. 079 * Those axis names will be verified by this validator, unless this fields is explicitly set to {@code false}. 080 * 081 * @see #validate(GeodeticCRS) 082 * @see #validate(ProjectedCRS) 083 * @see #validate(VerticalCRS) 084 * 085 * @since 3.1 086 */ 087 public boolean enforceStandardNames = true; 088 089 /** 090 * Creates a new validator instance. 091 * 092 * @param container the set of validators to use for validating other kinds of objects 093 * (see {@linkplain #container field javadoc}). 094 */ 095 public CRSValidator(final ValidatorContainer container) { 096 super(container, "org.opengis.referencing.crs"); 097 } 098 099 /** 100 * For each interface implemented by the given object, invokes the corresponding 101 * {@code validate(…)} method defined in this class (if any). 102 * 103 * @param object the object to dispatch to {@code validate(…)} methods, or {@code null}. 104 * @return number of {@code validate(…)} methods invoked in this class for the given object. 105 */ 106 @SuppressWarnings("SuspiciousIndentAfterControlStatement") 107 public int dispatch(final CoordinateReferenceSystem object) { 108 int n = 0; 109 if (object != null) { 110 if (object instanceof GeodeticCRS) {validate((GeodeticCRS) object); n++;} 111 if (object instanceof ProjectedCRS) {validate((ProjectedCRS) object); n++;} else 112 if (object instanceof DerivedCRS) {validate((DerivedCRS) object); n++;} // Implied by above case. 113 if (object instanceof EngineeringCRS) {validate((EngineeringCRS) object); n++;} 114 if (object instanceof VerticalCRS) {validate((VerticalCRS) object); n++;} 115 if (object instanceof TemporalCRS) {validate((TemporalCRS) object); n++;} 116 if (object instanceof CompoundCRS) {validate((CompoundCRS) object); n++;} 117 if (n == 0) { 118 validateIdentifiedObject(object); 119 container.validate(object.getCoordinateSystem()); 120 } 121 } 122 return n; 123 } 124 125 /** 126 * Validates the given coordinate reference system. If the {@link #enforceStandardNames} 127 * field is set to {@code true} (which is the default), then this method expects the axes 128 * to have the following names: 129 * 130 * <ul> 131 * <li>For ellipsoidal coordinate system, <q>geodetic latitude</q>, 132 * <q>geodetic longitude</q> and <q>ellipsoidal height</q> (if 3D).</li> 133 * <li>For Cartesian coordinate system, <q>geocentric X</q>, 134 * <q>geocentric Y</q> and <q>geocentric Z</q>.</li> 135 * <li>For spherical coordinate system, <q>spherical latitude</q>, 136 * <q>spherical longitude</q> and <q>geocentric radius</q>.</li> 137 * </ul> 138 * 139 * @param object the object to validate, or {@code null}. 140 * 141 * @since 3.1 142 */ 143 public void validate(final GeodeticCRS object) { 144 if (object == null) { 145 return; 146 } 147 validateIdentifiedObject(object); 148 final CoordinateSystem cs = object.getCoordinateSystem(); 149 mandatory(cs, "GeodeticCRS: shall have a CoordinateSystem."); 150 if (cs instanceof EllipsoidalCS) { 151 container.validate((EllipsoidalCS) cs); 152 if (enforceStandardNames) { 153 assertStandardNames("GeographicCRS", cs, GEOGRAPHIC_AXIS_NAME); 154 } 155 } else if (cs instanceof CartesianCS) { 156 container.validate((CartesianCS) cs); 157 final Set<AxisDirection> axes = getAxisDirections(cs); 158 validate(axes); 159 assertTrue(axes.remove(AxisDirection.GEOCENTRIC_X), "GeocentricCRS: expected Geocentric X axis direction."); 160 assertTrue(axes.remove(AxisDirection.GEOCENTRIC_Y), "GeocentricCRS: expected Geocentric Y axis direction."); 161 assertTrue(axes.remove(AxisDirection.GEOCENTRIC_Z), "GeocentricCRS: expected Geocentric Z axis direction."); 162 assertTrue(axes.isEmpty(), "GeocentricCRS: unknown axis direction."); 163 if (enforceStandardNames) { 164 assertStandardNames("GeocentricCRS", cs, GEOCENTRIC_AXIS_NAME); 165 } 166 } else if (cs instanceof SphericalCS) { 167 container.validate((SphericalCS) cs); 168 if (enforceStandardNames) { 169 assertStandardNames("GeocentricCRS", cs, SPHERICAL_AXIS_NAME); 170 } 171 } else if (cs != null) { 172 fail("GeodeticCRS: unknown CoordinateSystem of type " + cs.getClass().getCanonicalName() + '.'); 173 } 174 final GeodeticDatum datum = object.getDatum(); 175 mandatory(datum, "GeodeticCRS: shall have a Datum."); 176 container.validate(datum); 177 } 178 179 /** 180 * Validates the given coordinate reference system. If the {@link #enforceStandardNames} 181 * field is set to {@code true} (which is the default), then this method expects the axes 182 * to have the following names: 183 * 184 * <ul> 185 * <li><q>northing</q> or <q>southing</q>, <q>easting</q> or <q>westing</q>.</li> 186 * </ul> 187 * 188 * @param object the object to validate, or {@code null}. 189 */ 190 public void validate(final ProjectedCRS object) { 191 if (object == null) { 192 return; 193 } 194 validateIdentifiedObject(object); 195 196 final GeodeticCRS baseCRS = object.getBaseCRS(); 197 mandatory(baseCRS, "ProjectedCRS: shall have a base CRS."); 198 validate(baseCRS); 199 200 final CartesianCS cs = object.getCoordinateSystem(); 201 mandatory(cs, "ProjectedCRS: shall have a CoordinateSystem."); 202 container.validate(cs); 203 if (enforceStandardNames) { 204 assertStandardNames("ProjectedCRS", cs, PROJECTED_AXIS_NAME); 205 } 206 final GeodeticDatum datum = object.getDatum(); 207 mandatory(datum, "ProjectedCRS: shall have a Datum."); 208 container.validate(datum); 209 210 validateGeneralDerivedCRS(object); 211 } 212 213 /** 214 * Validates the given coordinate reference system. 215 * 216 * @param object the object to validate, or {@code null}. 217 */ 218 public void validate(final DerivedCRS object) { 219 if (object == null) { 220 return; 221 } 222 validateIdentifiedObject(object); 223 224 final CoordinateReferenceSystem baseCRS = object.getBaseCRS(); 225 mandatory(baseCRS, "DerivedCRS: shall have a base CRS."); 226 dispatch(baseCRS); 227 228 final CoordinateSystem cs = object.getCoordinateSystem(); 229 mandatory(cs, "DerivedCRS: shall have a CoordinateSystem."); 230 container.validate(cs); 231 232 final Datum datum = object.getDatum(); 233 mandatory(datum, "DerivedCRS: shall have a Datum."); 234 container.validate(datum); 235 236 validateGeneralDerivedCRS(object); 237 } 238 239 /** 240 * Validates the conversion in the given derived CRS. This method is private because 241 * it doesn't perform a full validation; only the one not already done by the public 242 * {@link #validate(ProjectedCRS)} and {@link #validate(DerivedCRS)} methods. 243 * 244 * @param object the object to validate, or {@code null}. 245 */ 246 private void validateGeneralDerivedCRS(final DerivedCRS object) { 247 if (!Boolean.TRUE.equals(VALIDATING.get())) try { 248 VALIDATING.set(Boolean.TRUE); 249 final Conversion conversion = object.getConversionFromBase(); 250 if (conversion != null) { 251 container.validate(conversion); 252 final CoordinateReferenceSystem baseCRS = object.getBaseCRS(); 253 final CoordinateReferenceSystem sourceCRS = conversion.getSourceCRS(); 254 final CoordinateReferenceSystem targetCRS = conversion.getTargetCRS(); 255 if (baseCRS != null && sourceCRS != null) { 256 assertSame(baseCRS, sourceCRS, 257 "DerivedCRS: The base CRS should be the source CRS of the conversion."); 258 } 259 if (targetCRS != null) { 260 assertSame(object, targetCRS, 261 "DerivedCRS: The derived CRS should be the target CRS of the conversion."); 262 } 263 } 264 } finally { 265 VALIDATING.set(Boolean.FALSE); 266 } 267 } 268 269 /** 270 * Validates the given coordinate reference system. 271 * 272 * @param object the object to validate, or {@code null}. 273 * 274 * @deprecated {@code ImageCRS} is replaced by {@link EngineeringCRS} as of ISO 19111:2019. 275 */ 276 @Deprecated(since="3.1") 277 public void validate(final ImageCRS object) { 278 if (object == null) { 279 return; 280 } 281 validateIdentifiedObject(object); 282 final AffineCS cs = object.getCoordinateSystem(); 283 mandatory(cs, "ImageCRS: shall have a CoordinateSystem."); 284 container.validate(cs); 285 286 final ImageDatum datum = object.getDatum(); 287 mandatory(datum, "ImageCRS: shall have a Datum."); 288 container.validate(datum); 289 } 290 291 /** 292 * Validates the given coordinate reference system. 293 * 294 * @param object the object to validate, or {@code null}. 295 */ 296 @SuppressWarnings("deprecation") 297 public void validate(final EngineeringCRS object) { 298 if (object == null) { 299 return; 300 } 301 validateIdentifiedObject(object); 302 final CoordinateSystem cs = object.getCoordinateSystem(); 303 mandatory(cs, "EngineeringCRS: shall have a CoordinateSystem."); 304 container.validate(cs); 305 String message = "EngineeringCRS: illegal coordinate system type. Shall be one of" 306 + " affine, Cartesian, cylindrical, linear, polar, or spherical."; 307 assertTrue(cs instanceof AffineCS || // Include the CartesianCS case. 308 cs instanceof CylindricalCS || 309 cs instanceof LinearCS || 310 cs instanceof PolarCS || 311 cs instanceof SphericalCS || 312 cs instanceof UserDefinedCS, 313 message); 314 315 assertFalse(cs instanceof EllipsoidalCS || 316 cs instanceof VerticalCS || 317 cs instanceof ParametricCS || 318 cs instanceof TimeCS, 319 message); 320 321 final Datum datum = object.getDatum(); 322 mandatory(datum, "EngineeringCRS: shall have a Datum."); 323 container.validate(datum); 324 } 325 326 /** 327 * Validates the given coordinate reference system. If the {@link #enforceStandardNames} 328 * field is set to {@code true} (which is the default), then this method expects the axes 329 * to have the following names: 330 * 331 * <ul> 332 * <li><q>depth</q> or <q>gravity-related height</q>.</li> 333 * </ul> 334 * 335 * @param object the object to validate, or {@code null}. 336 */ 337 public void validate(final VerticalCRS object) { 338 if (object == null) { 339 return; 340 } 341 validateIdentifiedObject(object); 342 final VerticalCS cs = object.getCoordinateSystem(); 343 mandatory(cs, "VerticalCRS: shall have a CoordinateSystem."); 344 container.validate(cs); 345 if (enforceStandardNames) { 346 assertStandardNames("VerticalCRS", cs, VERTICAL_AXIS_NAME); 347 } 348 final VerticalDatum datum = object.getDatum(); 349 mandatory(datum, "VerticalCRS: shall have a Datum."); 350 container.validate(datum); 351 } 352 353 /** 354 * Validates the given coordinate reference system. 355 * 356 * @param object the object to validate, or {@code null}. 357 */ 358 public void validate(final TemporalCRS object) { 359 if (object == null) { 360 return; 361 } 362 validateIdentifiedObject(object); 363 final TimeCS cs = object.getCoordinateSystem(); 364 mandatory(cs, "TemporalCRS: shall have a CoordinateSystem."); 365 container.validate(cs); 366 367 final TemporalDatum datum = object.getDatum(); 368 mandatory(datum, "TemporalCRS: shall have a Datum."); 369 container.validate(datum); 370 } 371 372 /** 373 * Validates the given coordinate reference system. 374 * This method will validate every individual components in the given compound CRS. 375 * 376 * @param object the object to validate, or {@code null}. 377 * 378 * @since 3.1 379 */ 380 public void validate(final CompoundCRS object) { 381 if (object == null) { 382 return; 383 } 384 validateIdentifiedObject(object); 385 final CoordinateSystem cs = object.getCoordinateSystem(); 386 mandatory(cs, "CompoundCRS: shall have a CoordinateSystem."); 387 container.validate(cs); 388 AxisDirection[] directions = null; 389 if (cs != null) { 390 directions = new AxisDirection[cs.getDimension()]; 391 for (int i=0; i<directions.length; i++) { 392 CoordinateSystemAxis axis = cs.getAxis(i); 393 if (axis != null) { 394 directions[i] = axis.getDirection(); 395 } 396 } 397 } 398 /* 399 * Verify the components, potentially with nested compound CRS. 400 */ 401 final List<CoordinateReferenceSystem> components = object.getComponents(); 402 mandatory(components, "CompoundCRS: shall have components."); 403 if (components != null) { 404 int dimension = 0; 405 // If the above 'mandatory(…)' call accepted an empty list, we accept it too. 406 assertNotEquals(1, components.size(), "CompoundCRS: shall have at least 2 components."); 407 for (final CoordinateReferenceSystem component : components) { 408 dispatch(component); 409 dimension = compareAxes(component.getCoordinateSystem(), directions, dimension); 410 } 411 if (directions != null) { 412 assertEquals(directions.length, dimension, "CompoundCRS: unexpected sum of the dimension of components."); 413 } 414 } 415 /* 416 * Verify the components again, but without nested compound CRS. 417 */ 418 final List<SingleCRS> singles = object.getSingleComponents(); 419 mandatory(singles, "CompoundCRS: shall have components."); 420 if (singles != null) { 421 int dimension = 0; 422 assertNotEquals(1, singles.size(), "CompoundCRS: shall have at least 2 components."); 423 for (final SingleCRS component : singles) { 424 dispatch(component); 425 dimension = compareAxes(component.getCoordinateSystem(), directions, dimension); 426 } 427 if (directions != null) { 428 assertEquals(directions.length, dimension, "CompoundCRS: unexpected sum of the dimension of components."); 429 } 430 } 431 } 432 433 /** 434 * Checks if the axis directions of the given coordinate system are the expected one. 435 * 436 * @param cs the coordinate system to validate, or {@code null} if none. 437 * @param directions the expected directions, or {@code null} if unknown. 438 * @param index index of the first element in the {@code directions} array. 439 * @return index after the last element in the {@code directions} array. 440 */ 441 private static int compareAxes(final CoordinateSystem cs, final AxisDirection[] directions, int index) { 442 if (directions != null) { 443 assertNotNull("CompoundCRS: missing coordinate system for component."); 444 final int dimension = cs.getDimension(); 445 assertTrue(index + dimension <= directions.length, "CompoundCRS: components have too many dimensions."); 446 for (int i=0; i<dimension; i++) { 447 final AxisDirection expected = directions[index++]; 448 if (expected != null) { 449 final int d = i; // Because lambda functions require final values. 450 final Supplier<String> message = () -> "CompoundCRS: inconsistent axis at dimension " + d + "."; 451 final CoordinateSystemAxis axis = cs.getAxis(d); 452 assertNotNull(axis, message); 453 assertEquals(expected, axis.getDirection(), message); 454 } 455 } 456 } 457 return index; 458 } 459 460 /** 461 * Verifies that the given coordinate system uses the given standard axis names. 462 * 463 * @param type type of coordinate system to report in error messages. 464 * @param cs the coordinate system to validate. 465 * @param standardNames the expected standard axis names. 466 */ 467 private static void assertStandardNames(final String type, final CoordinateSystem cs, final String[] standardNames) { 468 final int dimension = cs.getDimension(); 469 final Set<String> names = new LinkedHashSet<>(dimension * 4/3 + 1); 470 for (int i=0; i<dimension; i++) { 471 final String name = getName(cs.getAxis(i)); 472 if (name != null && !names.add(toLowerCase(name.trim()))) { 473 fail(type + ": duplicated axis name: " + name); 474 } 475 } 476 final List<String> notFound = new ArrayList<>(names.size()); 477 for (final String name : standardNames) { 478 if (!names.remove(name)) { 479 notFound.add(name); 480 } 481 } 482 if (!names.isEmpty()) { 483 fail(type + ": Non-standard axis names: " + names + ". Expected some of " + notFound + '.'); 484 } 485 } 486 487 /** 488 * Returns the given string in lower cases, except for the last letter if it is single. 489 * The intent is to leave the trailing X, Y or Z case unchanged in "geocentric X", 490 * "geocentric Y" and "geocentric Z" axis names. 491 * 492 * @param name the name in mixed case. 493 * @return the given string in lower case, except last letter if single. 494 */ 495 static String toLowerCase(final String name) { 496 int s = name.length(); 497 if (s >= 3) { 498 s -= Character.charCount(name.codePointBefore(s)); 499 final int c = name.codePointBefore(s); 500 if (Character.isSpaceChar(c)) { 501 s -= Character.charCount(c); 502 return name.substring(0, s).toLowerCase().concat(name.substring(s)); 503 } 504 } 505 return name.toLowerCase(); 506 } 507}