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.Collection;
021import java.util.function.Consumer;
022import java.util.function.Supplier;
023import javax.measure.Unit;
024import javax.measure.quantity.Angle;
025import javax.measure.quantity.Length;
026
027import org.opengis.referencing.datum.*;
028import org.opengis.referencing.IdentifiedObject;
029import org.opengis.test.ValidatorContainer;
030
031import static java.lang.Double.POSITIVE_INFINITY;
032import static org.junit.jupiter.api.Assertions.*;
033import static org.opengis.test.Assertions.assertBetween;
034
035
036/**
037 * Validates {@link Datum} and related objects from the {@code org.opengis.datum} package.
038 *
039 * <p>This class is provided for users wanting to override the validation methods. When the default
040 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
041 * convenient way to validate various kinds of objects.</p>
042 *
043 * @author  Martin Desruisseaux (Geomatys)
044 * @version 3.1
045 * @since   2.2
046 */
047public class DatumValidator extends ReferencingValidator {
048    /**
049     * Creates a new validator instance.
050     *
051     * @param container  the set of validators to use for validating other kinds of objects
052     *                   (see {@linkplain #container field javadoc}).
053     */
054    public DatumValidator(ValidatorContainer container) {
055        super(container, "org.opengis.referencing.datum");
056    }
057
058    /**
059     * For each interface implemented by the given object, invokes the corresponding
060     * {@code validate(…)} method defined in this class (if any).
061     *
062     * @param  object  the object to dispatch to {@code validate(…)} methods, or {@code null}.
063     * @return number of {@code validate(…)} methods invoked in this class for the given object.
064     */
065    @SuppressWarnings("deprecation")
066    public int dispatch(final Datum object) {
067        int n = 0;
068        if (object != null) {
069            if (object instanceof GeodeticDatum)    {validate((GeodeticDatum)    object); n++;}
070            if (object instanceof VerticalDatum)    {validate((VerticalDatum)    object); n++;}
071            if (object instanceof TemporalDatum)    {validate((TemporalDatum)    object); n++;}
072            if (object instanceof ImageDatum)       {validate((ImageDatum)       object); n++;}
073            if (object instanceof EngineeringDatum) {validate((EngineeringDatum) object); n++;}
074            if (object instanceof DatumEnsemble)    {validate((DatumEnsemble)    object); n++;}
075            if (n == 0) {
076                validateIdentifiedObject(object);
077            }
078        }
079        return n;
080    }
081
082    /**
083     * Validates the given prime meridian.
084     *
085     * @param  object  the object to validate, or {@code null}.
086     */
087    public void validate(final PrimeMeridian object) {
088        if (object == null) {
089            return;
090        }
091        validateIdentifiedObject(object);
092        final Unit<Angle> unit = object.getAngularUnit();
093        mandatory(unit, "PrimeMeridian: shall have a unit of measurement.");
094        double longitude = object.getGreenwichLongitude();
095        if (unit != null) {
096            final Unit<Angle> degree = units.degree();
097            assertTrue(unit.isCompatible(degree), "PrimeMeridian: unit must be compatible with degrees.");
098            longitude = unit.getConverterTo(degree).convert(longitude);
099        }
100        assertBetween(-180, +180, longitude, "PrimeMeridian: expected longitude in [-180 … +180]° range.");
101    }
102
103    /**
104     * Validates the given ellipsoid.
105     * This method checks the following conditions:
106     *
107     * <ul>
108     *   <li>{@linkplain Ellipsoid#getAxisUnit() Axis unit} is defined and is linear.</li>
109     *   <li>{@linkplain Ellipsoid#getSemiMinorAxis() semi-minor} &lt;= {@linkplain Ellipsoid#getSemiMajorAxis() semi-major}.</li>
110     *   <li>{@linkplain Ellipsoid#getInverseFlattening() inverse flattening} &gt; 0.</li>
111     *   <li>Consistency of semi-minor axis length with inverse flattening factor.</li>
112     * </ul>
113     *
114     * @param  object  the object to validate, or {@code null}.
115     */
116    public void validate(final Ellipsoid object) {
117        if (object == null) {
118            return;
119        }
120        validateIdentifiedObject(object);
121        final Unit<Length> unit = object.getAxisUnit();
122        mandatory(unit, "Ellipsoid: shall have a unit of measurement.");
123        if (unit != null) {
124            assertTrue(unit.isCompatible(units.metre()), "Ellipsoid: unit must be compatible with metres.");
125        }
126        final double semiMajor         = object.getSemiMajorAxis();
127        final double semiMinor         = object.getSemiMinorAxis();
128        final double inverseFlattening = object.getInverseFlattening();
129        assertTrue(semiMajor > 0,                 invalidAxisLength("semi-major", '>', 0, semiMajor));
130        assertTrue(semiMinor > 0,                 invalidAxisLength("semi-minor", '>', 0, semiMinor));
131        assertTrue(semiMajor < POSITIVE_INFINITY, invalidAxisLength("semi-major", '<', POSITIVE_INFINITY, semiMajor));
132        assertTrue(semiMinor <= semiMajor,        invalidAxisLength("semi-minor", '≤', semiMajor, semiMinor));
133        assertTrue(inverseFlattening > 0, "Ellipsoid: expected inverse flattening > 0.");
134        object.getSemiMedianAxis().ifPresent((semiMedian) -> {
135            assertTrue(semiMedian > semiMinor, invalidAxisLength("semi-median", '>', semiMinor, semiMedian));
136            assertTrue(semiMedian < semiMajor, invalidAxisLength("semi-median", '<', semiMajor, semiMedian));
137        });
138        if (!object.isSphere()) {
139            assertEquals(semiMajor - semiMajor/inverseFlattening, semiMinor, semiMinor*DEFAULT_TOLERANCE,
140                         "Ellipsoid: inconsistent semi-major axis length.");
141            assertEquals(semiMajor / (semiMajor-semiMinor), inverseFlattening, inverseFlattening*DEFAULT_TOLERANCE,
142                         "Ellipsoid: inconsistent inverse flattening.");
143        }
144    }
145
146    /**
147     * Returns a supplier of message to format in case of error while verifying the length of an ellipsoid axis.
148     *
149     * @param  name      name of the ellipsoid axis to verify.
150     * @param  operator  the mathematical operator used for comparing the actual value with the limit.
151     * @param  limit     minimum or maximum expected value.
152     * @param  actual    the actual value found in the ellipsoid.
153     * @return the message to format if the assertion fails.
154     */
155    private static Supplier<String> invalidAxisLength(String name, char operator, double limit, double actual) {
156        return () -> "Ellipsoid: expected " + name + " axis length " + operator + ' ' + limit + " but got " + actual + '.';
157    }
158
159    /**
160     * Validates the given datum.
161     *
162     * @param  object  the object to validate, or {@code null}.
163     */
164    public void validate(final GeodeticDatum object) {
165        if (object == null) {
166            return;
167        }
168        validateIdentifiedObject(object);
169        final PrimeMeridian meridian = object.getPrimeMeridian();
170        mandatory(meridian, "GeodeticDatum: shall have a prime meridian.");
171        validate(meridian);
172
173        final Ellipsoid ellipsoid = object.getEllipsoid();
174        mandatory(ellipsoid, "GeodeticDatum: shall have an ellipsoid.");
175        validate(ellipsoid);
176    }
177
178    /**
179     * Validates the given datum.
180     *
181     * @param  object  the object to validate, or {@code null}.
182     */
183    public void validate(final VerticalDatum object) {
184        if (object == null) {
185            return;
186        }
187        validateIdentifiedObject(object);
188    }
189
190    /**
191     * Validates the given datum.
192     *
193     * @param  object  the object to validate, or {@code null}.
194     */
195    public void validate(final TemporalDatum object) {
196        if (object == null) {
197            return;
198        }
199        validateIdentifiedObject(object);
200        final var origin = object.getOrigin();
201        mandatory(origin, "TemporalDatum: expected an origin.");
202    }
203
204    /**
205     * Validates the given datum.
206     *
207     * @param  object  the object to validate, or {@code null}.
208     *
209     * @deprecated Replaced by {@link EngineeringDatum} as of ISO 19111:2019.
210     */
211    @Deprecated(since="3.1")
212    public void validate(final ImageDatum object) {
213        if (object == null) {
214            return;
215        }
216        validateIdentifiedObject(object);
217        final PixelInCell pc = object.getPixelInCell();
218        mandatory(pc, "ImageDatum: shall specify PixelInCell.");
219    }
220
221    /**
222     * Validates the given datum.
223     *
224     * @param  object  the object to validate, or {@code null}.
225     */
226    public void validate(final EngineeringDatum object) {
227        if (object == null) {
228            return;
229        }
230        validateIdentifiedObject(object);
231    }
232
233    /**
234     * Validates the given datum ensemble.
235     *
236     * @param  object  the object to validate, or {@code null}.
237     *
238     * @since 3.1
239     */
240    public void validate(final DatumEnsemble<?> object) {
241        if (object == null) {
242            return;
243        }
244        validateIdentifiedObject(object);
245        final Collection<? extends Datum> members = object.getMembers();
246        assertNotNull(members, "DatumEnsemble: shall have at least two members.");
247        assertTrue(members.size() >= 2, "DatumEnsemble: shall have at least two members.");
248        final Consumer<IdentifiedObject> c = new Consumer<>() {
249            /** The first conventional RS found in datum. */
250            private IdentifiedObject conventionalRS;
251
252            /** Verifies that the given conventional RS is the same as in previous datum. */
253            @Override public void accept(final IdentifiedObject rs) {
254                if (conventionalRS == null) {
255                    conventionalRS = rs;
256                } else {
257                    assertEquals(conventionalRS, rs, "Members of datum ensemble shall share the same conventional RS.");
258                }
259            }
260        };
261        for (final Datum member : members) {
262            dispatch(member);
263            member.getConventionalRS().ifPresent(c);
264        }
265        container.validate(object.getEnsembleAccuracy());
266    }
267}