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