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