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}