blob: d2a66ce4162ec9b1c8031ac4c230bcfce28f274e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
import java.time.Instant;
import java.text.SimpleDateFormat;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.EllipsoidalCS;
import org.opengis.referencing.datum.TemporalDatum;
import org.opengis.referencing.datum.VerticalDatum;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.privy.AxisNames;
import org.apache.sis.referencing.internal.VerticalDatumTypes;
import org.apache.sis.util.privy.Constants;
// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.opengis.test.Validators;
import org.apache.sis.test.TestCase;
import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
import static org.apache.sis.test.Assertions.assertMessageContains;
import static org.apache.sis.test.TestUtilities.*;
import static org.apache.sis.util.privy.Constants.UTC;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.datum.RealizationMethod;
import static org.opengis.test.Assertions.assertAxisDirectionsEqual;
import org.apache.sis.util.privy.TemporalDate;
/**
* Tests the {@link CommonCRS} class.
*
* @author Martin Desruisseaux (IRD, Geomatys)
*/
public final class CommonCRSTest extends TestCase {
/**
* Creates a new test case.
*/
public CommonCRSTest() {
}
/**
* Verifies that the same EPSG code is not used for two objects. Collisions are not allowed between
* {@code geographic}, {@code geocentric}, {@code geo3D} and all UTM projections. Strictly speaking
* all the above-cited codes may collide with {@code datum} and {@code ellipsoid}, but we nevertheless
* avoid those collisions in order to simplify {@link EPSGFactoryFallback#createObject(String)} implementation.
*/
@Test
public void ensureNoCodeCollision() {
final Map<Integer,Enum<?>> codes = new HashMap<>();
final CommonCRS[] values = CommonCRS.values();
for (final CommonCRS crs : values) {
assertNoCodeCollision(codes, crs, crs.geographic);
assertNoCodeCollision(codes, crs, crs.geocentric);
assertNoCodeCollision(codes, crs, crs.geo3D);
assertNoCodeCollision(codes, crs, crs.northUPS);
assertNoCodeCollision(codes, crs, crs.southUPS);
for (int zone = crs.firstZone; zone <= crs.lastZone; zone++) {
if (crs.northUTM != 0) assertNoCodeCollision(codes, crs, crs.northUTM + zone);
if (crs.southUTM != 0) assertNoCodeCollision(codes, crs, crs.southUTM + zone);
}
}
final CommonCRS.Vertical[] vertical = CommonCRS.Vertical.values();
for (final CommonCRS.Vertical crs : vertical) {
if (crs.isEPSG) {
assertNoCodeCollision(codes, crs, crs.crs);
}
}
/*
* Following restrictions are not strictly required, but their enforcement
* simplifies the EPSGFactoryFallback.createObject(String) implementation.
*/
assertNull(codes.put(Integer.valueOf(StandardDefinitions.GREENWICH), CommonCRS.WGS84));
for (final CommonCRS crs : values) assertNull(codes.get(Integer.valueOf(crs.ellipsoid)), crs.name());
for (final CommonCRS crs : values) assertNotSame(crs, codes.put(Integer.valueOf(crs.ellipsoid), crs), crs.name());
for (final CommonCRS crs : values) assertNull(codes.get(Integer.valueOf(crs.datum)), crs.name());
for (final CommonCRS.Vertical crs : vertical) {
if (crs.isEPSG) {
assertNull(codes.get(Integer.valueOf(crs.datum)), crs.name());
}
}
}
/**
* Helper method for {@link #ensureNoCodeCollision()} only.
*/
private static void assertNoCodeCollision(final Map<Integer,Enum<?>> codes, final Enum<?> crs, final int n) {
if (n != 0) {
final Enum<?> existing = codes.put(n, crs);
if (existing != null) {
fail(existing + " and " + crs + " both use the same EPSG:" + n + " code.");
}
}
}
/**
* Tests the {@link CommonCRS#geographic()} method.
*/
@Test
public void testGeographic() {
final GeographicCRS geographic = CommonCRS.WGS84.geographic();
Validators.validate(geographic);
GeodeticObjectVerifier.assertIsWGS84(geographic, true, true);
assertSame(geographic, CommonCRS.WGS84.geographic(), "Cached value");
}
/**
* Tests the {@link CommonCRS#normalizedGeographic()} method.
*/
@Test
public void testNormalizedGeographic() {
final GeographicCRS geographic = CommonCRS.WGS84.geographic();
final GeographicCRS normalized = CommonCRS.WGS84.normalizedGeographic();
Validators.validate(normalized);
assertSame(geographic.getDatum(), normalized.getDatum());
/*
* Compare axes. Note that axes in different order have different EPSG codes.
*/
final CoordinateSystem φλ = geographic.getCoordinateSystem();
final CoordinateSystem λφ = normalized.getCoordinateSystem();
assertEqualsIgnoreMetadata(φλ.getAxis(1), λφ.getAxis(0)); // Longitude
assertEqualsIgnoreMetadata(φλ.getAxis(0), λφ.getAxis(1)); // Latitude
assertSame(normalized, CommonCRS.WGS84.normalizedGeographic(), "Cached value");
}
/**
* Tests the {@link CommonCRS#geographic3D()} method.
*/
@Test
public void testGeographic3D() {
final GeographicCRS crs = CommonCRS.WGS72.geographic3D();
Validators.validate(crs);
assertEquals ("WGS 72", crs.getName().getCode());
assertSame (CommonCRS.WGS72.geographic().getDatum(), crs.getDatum());
assertNotSame(CommonCRS.WGS84.geographic().getDatum(), crs.getDatum());
final EllipsoidalCS cs = crs.getCoordinateSystem();
final String name = cs.getName().getCode();
assertTrue(name.startsWith("Ellipsoidal 3D"), name);
assertEquals(3, cs.getDimension(), "dimension");
assertAxisDirectionsEqual(cs, AxisDirection.NORTH, AxisDirection.EAST, AxisDirection.UP);
assertSame(crs, CommonCRS.WGS72.geographic3D(), "Cached value");
}
/**
* Tests the {@link CommonCRS#geocentric()} method.
*/
@Test
public void testGeocentric() {
final GeodeticCRS crs = CommonCRS.WGS72.geocentric();
Validators.validate(crs);
assertEquals ("WGS 72", crs.getName().getCode());
assertSame (CommonCRS.WGS72.geographic().getDatum(), crs.getDatum());
assertNotSame(CommonCRS.WGS84.geographic().getDatum(), crs.getDatum());
final CoordinateSystem cs = crs.getCoordinateSystem();
final String name = cs.getName().getCode();
assertTrue(name.startsWith("Cartesian 3D"), name);
assertEquals(3, cs.getDimension(), "dimension");
assertAxisDirectionsEqual(cs, AxisDirection.GEOCENTRIC_X, AxisDirection.GEOCENTRIC_Y, AxisDirection.GEOCENTRIC_Z);
assertSame(crs, CommonCRS.WGS72.geocentric(), "Cached value");
}
/**
* Tests the {@link CommonCRS#spherical()} method.
*/
@Test
public void testSpherical() {
final GeodeticCRS crs = CommonCRS.ETRS89.spherical();
Validators.validate(crs);
assertEquals ("ETRS89", crs.getName().getCode());
assertSame (CommonCRS.ETRS89.geographic().getDatum(), crs.getDatum());
assertNotSame(CommonCRS.WGS84 .geographic().getDatum(), crs.getDatum());
final CoordinateSystem cs = crs.getCoordinateSystem();
final String name = cs.getName().getCode();
assertTrue(name.startsWith("Spherical"), name);
assertEquals(3, cs.getDimension(), "dimension");
assertAxisDirectionsEqual(cs, AxisDirection.NORTH, AxisDirection.EAST, AxisDirection.UP);
assertSame(crs, CommonCRS.ETRS89.spherical(), "Cached value");
}
/**
* Verifies the vertical datum enumeration.
*/
@Test
@SuppressWarnings("deprecation")
public void testVertical() {
for (final CommonCRS.Vertical e : CommonCRS.Vertical.values()) {
final RealizationMethod method;
final String axisName, datumName;
switch (e) {
case NAVD88: axisName = AxisNames.GRAVITY_RELATED_HEIGHT; datumName = "North American Vertical Datum 1988"; method = RealizationMethod. GEOID; break;
case BAROMETRIC: axisName = "Barometric altitude"; datumName = "Constant pressure surface"; method = RealizationMethod.valueOf("BAROMETRIC"); break;
case MEAN_SEA_LEVEL: axisName = AxisNames.GRAVITY_RELATED_HEIGHT; datumName = "Mean Sea Level"; method = RealizationMethod. TIDAL; break;
case DEPTH: axisName = AxisNames.DEPTH; datumName = "Mean Sea Level"; method = RealizationMethod. TIDAL; break;
case ELLIPSOIDAL: axisName = AxisNames.ELLIPSOIDAL_HEIGHT; datumName = "Ellipsoid"; method = VerticalDatumTypes.ellipsoidal(); break;
case OTHER_SURFACE: axisName = "Height"; datumName = "Other surface"; method = null; break;
default: throw new AssertionError(e);
}
final String name = e.name();
final VerticalDatum datum = e.datum();
final VerticalCRS crs = e.crs();
if (e.isEPSG) {
/*
* BAROMETRIC and ELLIPSOIDAL uses an axis named "Height", which is not a valid
* axis name according ISO 19111. We skip the validation test for those enums.
*/
Validators.validate(crs);
}
assertSame(datum, e.datum(), name); // Datum before CRS creation.
assertSame(crs.getDatum(), e.datum(), name); // Datum after CRS creation.
assertEquals(datumName, datum.getName().getCode(), name);
assertEquals(axisName, crs.getCoordinateSystem().getAxis(0).getName().getCode(), name);
if (!e.isEPSG) { // Because the information is not in EPSG database 9.x.
assertEquals(method, datum.getRealizationMethod().orElse(null), name);
}
}
}
/**
* Verifies the epoch values of temporal enumeration compared to the Julian epoch.
*
* @see <a href="https://en.wikipedia.org/wiki/Julian_day">Wikipedia: Julian day</a>
*/
@Test
public void testTemporal() {
final var julianEpoch = TemporalDate.toInstant(CommonCRS.Temporal.JULIAN.datum().getOrigin());
final double SECONDS_PER_DAY = Constants.SECONDS_PER_DAY;
final double julianEpochSecond = julianEpoch.getEpochSecond() / SECONDS_PER_DAY;
assertTrue(julianEpochSecond < 0);
/*
* We need to use `java.text.DateFormat` rather than `Instant.parse(String)` because
* they have different policy regarding the calendar for dates before October 15, 1582.
* The `java.time` classes use the proleptic Gregorian calendar while `java.text` uses
* the prolectic Julian calendar. The latter is what we need for this test.
*/
final var dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CANADA);
dateFormat.setTimeZone(TimeZone.getTimeZone(UTC));
dateFormat.setLenient(false);
for (final CommonCRS.Temporal e : CommonCRS.Temporal.values()) {
final String epoch;
final double days;
switch (e) {
case JAVA: // Fall through
case UNIX: epoch = "1970-01-01 00:00:00"; days = 2440587.5; break;
case TRUNCATED_JULIAN: epoch = "1968-05-24 00:00:00"; days = 2440000.5; break;
case DUBLIN_JULIAN: epoch = "1899-12-31 12:00:00"; days = 2415020.0; break;
case MODIFIED_JULIAN: epoch = "1858-11-17 00:00:00"; days = 2400000.5; break;
case JULIAN: epoch = "4713-01-01 12:00:00"; days = 0; break;
case TROPICAL_YEAR: epoch = "2000-01-01 00:00:00"; days = 2451544.5; break;
default: throw new AssertionError(e);
}
final String name = e.name();
final TemporalDatum datum = e.datum();
final TemporalCRS crs = e.crs();
final Date origin = datum.getOrigin();
Validators.validate(crs);
assertSame(datum, e.datum(), name); // Datum before CRS creation.
assertSame(crs.getDatum(), e.datum(), name); // Datum after CRS creation.
assertEquals(epoch, dateFormat.format(origin), name);
assertEquals(days, origin.getTime() / (1000*SECONDS_PER_DAY) - julianEpochSecond, name);
switch (e) {
case JAVA: {
assertNameContains(datum, "Unix/POSIX");
assertNameContains(crs, "Java");
break;
}
case UNIX: {
assertNameContains(datum, "Unix/POSIX");
assertNameContains(crs, "Unix/POSIX");
break;
}
}
}
}
/**
* Verifies that the name of given object contains the given word.
*/
private static void assertNameContains(final IdentifiedObject object, final String word) {
final String name = object.getName().getCode();
assertTrue(name.contains(word), name);
}
/**
* Tests {@link CommonCRS.Temporal#forIdentifier(String, boolean)}.
*/
@Test
public void testTemporalForIdentifier() {
assertSame(CommonCRS.Temporal.TRUNCATED_JULIAN, CommonCRS.Temporal.forIdentifier("TruncatedJulianDate", false));
assertSame(CommonCRS.Temporal.TRUNCATED_JULIAN, CommonCRS.Temporal.forIdentifier("TruncatedJulianDate", true));
assertSame(CommonCRS.Temporal.MODIFIED_JULIAN, CommonCRS.Temporal.forIdentifier("ModifiedJulianDate", false));
var exception = assertThrows(IllegalArgumentException.class,
() -> CommonCRS.Temporal.forIdentifier("ModifiedJulianDate", true),
"Unexpected because not in OGC namespace.");
assertMessageContains(exception, "ModifiedJulianDate", "OGC");
assertEquals("OGC:TruncatedJulianDate", getSingleton(CommonCRS.Temporal.TRUNCATED_JULIAN.crs().getIdentifiers()).toString());
assertEquals("SIS:ModifiedJulianDate", getSingleton(CommonCRS.Temporal. MODIFIED_JULIAN.crs().getIdentifiers()).toString());
}
/**
* Tests the URN lookup on temporal CRS.
*
* @throws FactoryException if a call to {@link IdentifiedObjects#lookupURN lookupURN(…)} failed.
*/
@Test
public void testLookupURN() throws FactoryException {
final TemporalCRS crs = CommonCRS.Temporal.TRUNCATED_JULIAN.crs();
assertNull(IdentifiedObjects.lookupEPSG(crs)); // Not an EPSG code.
assertNull(IdentifiedObjects.lookupURN(crs, Citations.SIS)); // Not in SIS namespace.
assertEquals("urn:ogc:def:crs:OGC::TruncatedJulianDate", IdentifiedObjects.lookupURN(crs, Citations.OGC));
}
/**
* Tests {@link CommonCRS#universal(double, double)} with Universal Transverse Mercator (UTM) projections.
*/
@Test
public void testUTM() {
final ProjectedCRS crs = CommonCRS.WGS72.universal(-45, -122);
assertEquals("WGS 72 / UTM zone 10S", crs.getName().getCode());
final ParameterValueGroup pg = crs.getConversionFromBase().getParameterValues();
assertEquals( 0, pg.parameter(Constants.LATITUDE_OF_ORIGIN).doubleValue());
assertEquals( -123, pg.parameter(Constants.CENTRAL_MERIDIAN) .doubleValue());
assertEquals( 0.9996, pg.parameter(Constants.SCALE_FACTOR) .doubleValue());
assertEquals( 500000, pg.parameter(Constants.FALSE_EASTING) .doubleValue());
assertEquals(10000000, pg.parameter(Constants.FALSE_NORTHING) .doubleValue());
assertSame(crs, CommonCRS.WGS72.universal(-45, -122), "Expected a cached instance.");
assertNotSame(crs, CommonCRS.WGS72.universal(+45, -122), "Expected a new instance.");
}
/**
* Tests {@link CommonCRS#universal(double, double)} with Universal Polar Stereographic (UPS) projections.
*/
@Test
public void testUPS() {
final ProjectedCRS crs = CommonCRS.WGS72.universal(-85, -122);
assertEquals("WGS 72 / Universal Polar Stereographic South", crs.getName().getCode());
final ParameterValueGroup pg = crs.getConversionFromBase().getParameterValues();
assertEquals( -90, pg.parameter(Constants.LATITUDE_OF_ORIGIN).doubleValue());
assertEquals( 0, pg.parameter(Constants.CENTRAL_MERIDIAN) .doubleValue());
assertEquals( 0.994, pg.parameter(Constants.SCALE_FACTOR) .doubleValue());
assertEquals(2000000, pg.parameter(Constants.FALSE_EASTING) .doubleValue());
assertEquals(2000000, pg.parameter(Constants.FALSE_NORTHING) .doubleValue());
assertSame(crs, CommonCRS.WGS72.universal(-85, -122), "Expected a cached instance.");
assertNotSame(crs, CommonCRS.WGS72.universal(+85, -122), "Expected a new instance.");
}
/**
* Tests {@link CommonCRS#forDatum(CoordinateReferenceSystem)}.
*
* @sinc 0.8
*/
@Test
public void testForDatum() {
assertSame(CommonCRS.WGS84, CommonCRS.forDatum(CommonCRS.WGS84.geographic()));
assertSame(CommonCRS.WGS72, CommonCRS.forDatum(CommonCRS.WGS72.geographic()));
}
/**
* Tests {@link CommonCRS.Temporal#forEpoch(Instant)}.
*/
@Test
public void testForEpoch() {
assertSame(CommonCRS.Temporal.UNIX, CommonCRS.Temporal.forEpoch(Instant.ofEpochMilli(0))); // As specified in Javadoc.
}
/**
* Tests formatting in a {@link java.util.Formatter}.
*/
@Test
public void testFormat() {
assertEquals("World Geodetic System 1984", String.format("%s", CommonCRS.WGS84.datum()));
assertEquals("WORLD GEODETIC SYSTEM 1984", String.format("%S", CommonCRS.WGS84.datum()));
assertTrue(String.format("%#s", CommonCRS.WGS84.datum()).endsWith(":6326"));
}
}