| /* |
| * 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.io.wkt; |
| |
| import java.util.Map; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.time.Instant; |
| import java.text.ParsePosition; |
| import java.text.ParseException; |
| import javax.measure.Unit; |
| import javax.measure.quantity.Length; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.cs.*; |
| import org.opengis.referencing.crs.*; |
| import org.opengis.referencing.datum.*; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.NoninvertibleTransformException; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.opengis.parameter.ParameterValue; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.apache.sis.metadata.privy.AxisNames; |
| import org.apache.sis.referencing.privy.ReferencingFactoryContainer; |
| import org.apache.sis.referencing.cs.CoordinateSystems; |
| import org.apache.sis.referencing.datum.BursaWolfParameters; |
| import org.apache.sis.referencing.datum.DefaultGeodeticDatum; |
| import org.apache.sis.referencing.factory.GeodeticObjectFactory; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.referencing.operation.transform.LinearTransform; |
| import org.apache.sis.measure.Units; |
| import static org.apache.sis.util.privy.Constants.SECONDS_PER_DAY; |
| |
| // Test dependencies |
| import org.junit.jupiter.api.Test; |
| import static org.junit.jupiter.api.Assertions.*; |
| import org.apache.sis.test.TestCase; |
| import static org.apache.sis.test.Assertions.assertMessageContains; |
| import static org.apache.sis.test.Assertions.assertMultilinesEquals; |
| import static org.apache.sis.referencing.Assertions.assertAxisEquals; |
| import static org.apache.sis.referencing.Assertions.assertDiagonalEquals; |
| import static org.apache.sis.test.TestUtilities.getSingleton; |
| |
| |
| /** |
| * Tests {@link GeodeticObjectParser}. |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| */ |
| public final class GeodeticObjectParserTest extends TestCase { |
| /** |
| * The parser to use for the test. |
| */ |
| private GeodeticObjectParser parser; |
| |
| /** |
| * Creates a new test case. |
| */ |
| public GeodeticObjectParserTest() { |
| } |
| |
| /** |
| * Instantiates the parser to test. |
| */ |
| private void newParser(final Convention convention) { |
| parser = new GeodeticObjectParser(null, Map.of(), Symbols.getDefault(), null, null, null, |
| convention, Transliterator.DEFAULT, null, new ReferencingFactoryContainer()); |
| assertEquals(GeodeticObjectFactory.class.getCanonicalName(), parser.getPublicFacade()); |
| } |
| |
| /** |
| * Parses the given text. It is caller's responsibility to verify if some warnings have been emitted. |
| * |
| * @param type the expected object type. |
| * @param text the WKT string to parse. |
| * @return the parsed object. |
| * @throws ParseException if an error occurred during the parsing. |
| */ |
| private <T> T parseIgnoreWarnings(final Class<T> type, final String text) throws ParseException { |
| if (parser == null) { |
| newParser(Convention.DEFAULT); |
| } |
| final ParsePosition position = new ParsePosition(0); |
| final Object obj = parser.createFromWKT(text, position); |
| assertEquals(-1, position.getErrorIndex(), "errorIndex"); |
| assertEquals(text.length(), position.getIndex(), "index"); |
| assertInstanceOf(type, obj, "GeodeticObjectParser.parseObject"); |
| return type.cast(obj); |
| } |
| |
| /** |
| * Parses the given text and ensure that no warnings have been emitted. |
| * |
| * @param type the expected object type. |
| * @param text the WKT string to parse. |
| * @return the parsed object. |
| * @throws ParseException if an error occurred during the parsing. |
| */ |
| private <T> T parse(final Class<T> type, final String text) throws ParseException { |
| final T obj = parseIgnoreWarnings(type, text); |
| assertNull(parser.getAndClearWarnings(obj), "warnings"); |
| assertTrue(parser.ignoredElements.isEmpty(), "ignoredElements"); |
| return obj; |
| } |
| |
| /** |
| * Asserts that the name and (optionally) the EPSG identifier of the given object are equal to the given strings. |
| * As a special case if the given EPSG code is 0, then this method verifies that the given object has no identifier. |
| * |
| * <p>This method is similar to {@code assertEpsgNameAndIdentifierEqual(name, epsg, object)} except that |
| * the given name is not necessarily in the EPSG namespace and the EPSG code is allowed to be absent.</p> |
| * |
| * @param name the expected name. |
| * @param epsg the expected EPSG identifier, or {@code 0} if the object shall have no identifier. |
| * |
| * @see org.apache.sis.test.ReferencingAssert#assertEpsgNameAndIdentifierEqual(String, int, IdentifiedObject) |
| */ |
| static void assertNameAndIdentifierEqual(final String name, final int epsg, final IdentifiedObject object) { |
| final String message = object.getClass().getSimpleName(); |
| assertEquals(name, object.getName().getCode(), message); |
| if (epsg != 0) { |
| assertEquals(String.valueOf(epsg), getSingleton(object.getIdentifiers()).getCode(), message); |
| } else { |
| assertTrue(object.getIdentifiers().isEmpty(), message); |
| } |
| } |
| |
| /** |
| * Asserts that the given axis is a longitude axis. This method expects the name to be |
| * <q>Geodetic longitude</q> even if the WKT string contained a different name, |
| * because {@link GeodeticObjectParser} should have done the replacement. |
| */ |
| private static void assertLongitudeAxisEquals(final CoordinateSystemAxis axis) { |
| assertAxisEquals(AxisNames.GEODETIC_LONGITUDE, "λ", AxisDirection.EAST, -180, +180, Units.DEGREE, RangeMeaning.WRAPAROUND, axis); |
| } |
| |
| /** |
| * Asserts that the given axis is a latitude axis. This method expects the name to be |
| * <q>Geodetic latitude</q> even if the WKT string contained a different name, |
| * because {@link GeodeticObjectParser} should have done the replacement. |
| */ |
| private static void assertLatitudeAxisEquals(final CoordinateSystemAxis axis) { |
| assertAxisEquals(AxisNames.GEODETIC_LATITUDE, "φ", AxisDirection.NORTH, -90, +90, Units.DEGREE, RangeMeaning.EXACT, axis); |
| } |
| |
| /** |
| * Asserts the given axis is an axis of the given name without minimum and maximum values. |
| */ |
| private static void assertUnboundedAxisEquals(final String name, final String abbreviation, |
| final AxisDirection direction, final Unit<?> unit, final CoordinateSystemAxis axis) |
| { |
| assertAxisEquals(name, abbreviation, direction, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, unit, null, axis); |
| } |
| |
| /** |
| * Tests the parsing of an axis. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testAxis() throws ParseException { |
| CoordinateSystemAxis axis = parse(CoordinateSystemAxis.class, "AXIS[“(Y)”, geocentricY]"); |
| assertEquals("Y", axis.getName().getCode(), "name"); |
| assertEquals("Y", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(AxisDirection.GEOCENTRIC_Y, axis.getDirection(), "direction"); |
| assertEquals(Units.METRE, axis.getUnit(), "unit"); |
| |
| axis = parse(CoordinateSystemAxis.class, "AXIS[“latitude”,north,ORDER[1],ANGLEUNIT[“degree”,0.0174532925199433]]"); |
| assertEquals("Latitude", axis.getName().getCode(), "name"); |
| assertEquals("φ", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(AxisDirection.NORTH, axis.getDirection(), "direction"); |
| assertEquals(Units.DEGREE, axis.getUnit(), "unit"); |
| |
| axis = parse(CoordinateSystemAxis.class, "AXIS[“longitude”,EAST,order[2],UNIT[“degree”,0.0174532925199433]]"); |
| assertEquals("Longitude", axis.getName().getCode(), "name"); |
| assertEquals("λ", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(AxisDirection.EAST, axis.getDirection(), "direction"); |
| assertEquals(Units.DEGREE, axis.getUnit(), "unit"); |
| |
| axis = parse(CoordinateSystemAxis.class, "AXIS[“ellipsoidal height (h)”,up,ORDER[3],LengthUnit[“kilometre”,1000]]"); |
| assertEquals("Ellipsoidal height", axis.getName().getCode(), "name"); |
| assertEquals("h", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(AxisDirection.UP, axis.getDirection(), "direction"); |
| assertEquals(Units.KILOMETRE, axis.getUnit(), "unit"); |
| |
| axis = parse(CoordinateSystemAxis.class, "AXIS[“time (t)”,future,TimeUnit[“hour”,3600]]"); |
| assertEquals("Time", axis.getName().getCode(), "name"); |
| assertEquals("t", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(AxisDirection.FUTURE, axis.getDirection(), "direction"); |
| assertEquals(Units.HOUR, axis.getUnit(), "unit"); |
| |
| axis = parse(CoordinateSystemAxis.class, "AXIS[“easting (X)”,south,MERIDIAN[90,UNIT[“degree”,0.0174532925199433]]]"); |
| assertEquals("Easting", axis.getName().getCode(), "name"); |
| assertEquals("X", axis.getAbbreviation(), "abbreviation"); |
| assertEquals(CoordinateSystems.directionAlongMeridian(AxisDirection.SOUTH, 90), axis.getDirection(), "direction"); |
| assertEquals(Units.METRE, axis.getUnit(), "unit"); |
| } |
| |
| /** |
| * Tests the parsing of a geodetic reference frame from a WKT 2 string. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testDatum() throws ParseException { |
| final GeodeticDatum datum = parse(GeodeticDatum.class, |
| "DATUM[“Tananarive 1925”,\n" + |
| " ELLIPSOID[“International 1924”, 6378.388, 297.0, LENGTHUNIT[“kilometre”, 1000]],\n" + |
| " ANCHOR[“Tananarive observatory”]]"); |
| |
| assertNameAndIdentifierEqual("Tananarive 1925", 0, datum); |
| assertEquals("Tananarive observatory", datum.getAnchorDefinition().get().toString()); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("International 1924", 0, ellipsoid); |
| assertEquals(Units.KILOMETRE, ellipsoid.getAxisUnit(), "unit"); |
| assertEquals(6378.388, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(297, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| assertEquals(0, datum.getPrimeMeridian().getGreenwichLongitude(), "greenwichLongitude"); |
| } |
| |
| /** |
| * Tests the parsing of a geocentric CRS from a WKT 1 string. The parser |
| * shall replace the OGC 01-009 axis directions (OTHER, EAST, NORTH) by |
| * ISO 19111 axis directions (Geocentric X, Geocentric Y, Geocentric Z). |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testGeocentricCRS() throws ParseException { |
| final GeodeticCRS crs = parse(GeodeticCRS.class, |
| "GEOCCS[“Geocentric”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563, AUTHORITY[“EPSG”, “7030”]],\n" + |
| " AUTHORITY[“EPSG”, “6326”]],\n" + |
| " PRIMEM[“Greenwich”, 0.0, AUTHORITY[“EPSG”, “8901”]],\n" + |
| " UNIT[“metre”, 1.0],\n" + |
| " AXIS[“X”, OTHER],\n" + |
| " AXIS[“Y”, EAST],\n" + |
| " AXIS[“Z”, NORTH]]"); |
| |
| assertNameAndIdentifierEqual("Geocentric", 0, crs); |
| |
| final GeodeticDatum datum = crs.getDatum(); |
| assertNameAndIdentifierEqual("World Geodetic System 1984", 6326, datum); |
| assertNameAndIdentifierEqual("Greenwich", 8901, datum.getPrimeMeridian()); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("WGS84", 7030, ellipsoid); |
| assertEquals(6378137, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(298.257223563, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| // Verify that the OGC 01-009 axes have been relaced by ISO 19111 axes. |
| final CartesianCS cs = (CartesianCS) crs.getCoordinateSystem(); |
| assertEquals(3, cs.getDimension(), "dimension"); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_X, "X", AxisDirection.GEOCENTRIC_X, Units.METRE, cs.getAxis(0)); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Y, "Y", AxisDirection.GEOCENTRIC_Y, Units.METRE, cs.getAxis(1)); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Z, "Z", AxisDirection.GEOCENTRIC_Z, Units.METRE, cs.getAxis(2)); |
| } |
| |
| /** |
| * Tests the parsing of a geographic CRS from a WKT 1 string using (longitude, latitude) axis order. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testGeographicCRS() throws ParseException { |
| verifyGeographicCRS(0, parse(GeographicCRS.class, |
| "GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST],\n" + |
| " AXIS[“Latitude”, NORTH]]")); |
| } |
| |
| /** |
| * Tests the parsing of a geographic CRS from a WKT 1 string using (latitude, longitude) axis order. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testGeographicWithLatLonAxes() throws ParseException { |
| verifyGeographicCRS(1, parse(GeographicCRS.class, |
| "GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Latitude”, NORTH],\n" + |
| " AXIS[“Longitude”, EAST]]")); |
| } |
| |
| /** |
| * Tests the parsing of a geographic CRS from a WKT 1 string that does not declare explicitly the axes. |
| * |
| * @throws ParseException if the parsing failed. |
| * |
| * @see #testGeographicWithImplicitAxesInSeconds() |
| */ |
| @Test |
| public void testGeographicWithImplicitAxes() throws ParseException { |
| verifyGeographicCRS(0, parse(GeographicCRS.class, |
| "GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295]]")); |
| } |
| |
| /** |
| * Tests parsing geographic CRS with implicit axes in seconds instead of degrees. |
| * |
| * @throws ParseException if the parsing failed. |
| * |
| * @see #testGeographicWithImplicitAxes() |
| */ |
| @Test |
| public void testGeographicWithImplicitAxesInSeconds() throws ParseException { |
| final GeographicCRS crs = parse(GeographicCRS.class, |
| "GEOGCS[“NAD83 / NFIS Seconds”," + |
| "DATUM[“North_American_Datum_1983”,\n" + |
| "SPHEROID[“GRS 1980”, 6378137, 298.257222101]],\n" + |
| "PRIMEM[“Greenwich”, 0],\n" + |
| "UNIT[“Decimal_Second”, 4.84813681109536e-06],\n" + |
| "AUTHORITY[“EPSG”, “100001”]]"); |
| |
| assertNameAndIdentifierEqual("NAD83 / NFIS Seconds", 100001, crs); |
| |
| final GeodeticDatum datum = crs.getDatum(); |
| assertNameAndIdentifierEqual("North_American_Datum_1983", 0, datum); |
| assertNameAndIdentifierEqual("Greenwich", 0, datum.getPrimeMeridian()); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("GRS 1980", 0, ellipsoid); |
| assertEquals(6378137, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(298.257222101, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| final EllipsoidalCS cs = crs.getCoordinateSystem(); |
| final double secondsIn90 = 90*60*60; |
| CoordinateSystemAxis axis = cs.getAxis(0); |
| assertEquals(AxisNames.GEODETIC_LONGITUDE, axis.getName().getCode()); |
| assertEquals("λ", axis.getAbbreviation()); |
| assertEquals(AxisDirection.EAST, axis.getDirection()); |
| assertEquals(-secondsIn90*2, axis.getMinimumValue(), 1E-9); |
| assertEquals(+secondsIn90*2, axis.getMaximumValue(), 1E-9); |
| |
| axis = cs.getAxis(1); |
| assertEquals(AxisNames.GEODETIC_LATITUDE, axis.getName().getCode()); |
| assertEquals("φ", axis.getAbbreviation()); |
| assertEquals(AxisDirection.NORTH, axis.getDirection()); |
| assertEquals(-secondsIn90, axis.getMinimumValue(), 1E-9); |
| assertEquals(+secondsIn90, axis.getMaximumValue(), 1E-9); |
| } |
| |
| /** |
| * Tests the parsing of a geographic CRS from a WKT string with axes in wrong order according the |
| * {@code ORDER} elements. The {@code ORDER} elements are defined in WKT 2 (they did not existed in WKT 1), |
| * but the rest of the string is WKT 1. The SIS parser should sort the axes in the order declared |
| * in the {@code ORDER} elements. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testGeographicWithUnorderedAxes() throws ParseException { |
| verifyGeographicCRS(1, parse(GeographicCRS.class, |
| "GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST, order[2]],\n" + |
| " AXIS[“Latitude”, NORTH, order[1]]]")); |
| } |
| |
| /** |
| * Tests parsing of a CRS using a prime meridian other than Greenwich. The result depends on whether |
| * the parsing is standard compliant or if the parsing use {@link Convention#WKT1_COMMON_UNITS}. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testGeographicWithParisMeridian() throws ParseException { |
| String wkt = "GEOGCS[“NTF (Paris)”,\n" + |
| " DATUM[“Nouvelle Triangulation Française (Paris)”,\n" + |
| " SPHEROID[“Clarke 1880 (IGN)”, 6378249.2, 293.4660212936269]],\n" + |
| " PRIMEM[“Paris”, 2.5969213, AUTHORITY[“EPSG”, “8903”]],\n" + |
| " UNIT[“grad”, 0.015707963267948967],\n" + |
| " AXIS[“Latitude”, NORTH],\n" + |
| " AXIS[“Longitude”, EAST]]"; |
| |
| GeographicCRS crs = parse(GeographicCRS.class, wkt); |
| assertNameAndIdentifierEqual("NTF (Paris)", 0, crs); |
| PrimeMeridian pm = verifyNTF(crs.getDatum(), false); |
| assertEquals(Units.GRAD, pm.getAngularUnit(), "angularUnit"); |
| assertEquals(2.5969213, pm.getGreenwichLongitude(), "greenwichLongitude"); |
| EllipsoidalCS cs = crs.getCoordinateSystem(); |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertAxisEquals(AxisNames.GEODETIC_LATITUDE, "φ", AxisDirection.NORTH, -100, +100, Units.GRAD, RangeMeaning.EXACT, cs.getAxis(0)); |
| assertAxisEquals(AxisNames.GEODETIC_LONGITUDE, "λ", AxisDirection.EAST, -200, +200, Units.GRAD, RangeMeaning.WRAPAROUND, cs.getAxis(1)); |
| /* |
| * Parse again using Convention.WKT1_COMMON_UNITS and ignoring AXIS[…] elements (but not the UNIT[…] element, |
| * which still apply to the axes). When using this convention, the parser does not apply the angular units to |
| * the Greenwich longitude value in PRIMEM[…] elements. Instead, the longitude is unconditionally interpreted |
| * as a value in degrees, which is why we convert "2.5969213" grad to "2.33722917" degrees in the WKT before |
| * to run the test (but we do NOT change the UNIT[…] element since the purpose of this test is to verify that |
| * those units are ignored). |
| * |
| * This is a violation of both OGC 01-009 and ISO 19162 standards, but this is what GDAL does. |
| * So we allow this interpretation in `Convention.WKT1_COMMON_UNITS` for compatibility reasons. |
| */ |
| wkt = wkt.replace("2.5969213", "2.33722917"); // Convert unit in prime meridian. |
| newParser(Convention.WKT1_IGNORE_AXES); |
| crs = parse(GeographicCRS.class, wkt); |
| assertNameAndIdentifierEqual("NTF (Paris)", 0, crs); |
| pm = verifyNTF(crs.getDatum(), false); |
| assertEquals(Units.DEGREE, pm.getAngularUnit(), "angularUnit"); |
| assertEquals(2.33722917, pm.getGreenwichLongitude(), "greenwichLongitude"); |
| cs = crs.getCoordinateSystem(); |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertAxisEquals(AxisNames.GEODETIC_LONGITUDE, "λ", AxisDirection.EAST, -200, +200, Units.GRAD, RangeMeaning.WRAPAROUND, cs.getAxis(0)); |
| assertAxisEquals(AxisNames.GEODETIC_LATITUDE, "φ", AxisDirection.NORTH, -100, +100, Units.GRAD, RangeMeaning.EXACT, cs.getAxis(1)); |
| } |
| |
| /** |
| * Tests parsing of a CRS with a prime meridian having implicit unit in grads but axes having explicit unit |
| * in degrees. The specification in §8.2.2 (ii) said: |
| * |
| * "(snip) the prime meridian’s {@literal <irm longitude>} value shall be given in |
| * the same angular units as those for the horizontal axes of the geographic CRS." |
| * |
| * Consequently, we expect the prime meridian to be in decimal degrees even if the WKT used in this test has |
| * an {@code Unit[“grad”, 0.015707963267948967]} element, because this WK also declare the axis as being in |
| * degrees. Since this can be confusing, we expect the parser to emit a warning. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testMismatchedAngularUnits() throws ParseException { |
| String wkt = "GeodeticCRS[“NTF (Paris)”,\n" + |
| " Datum[“Nouvelle Triangulation Française (Paris)”,\n" + |
| " Ellipsoid[“Clarke 1880 (IGN)”, 6378249.2, 293.4660212936269]],\n" + |
| " PrimeMeridian[“Paris”, 2.33722917],\n" + // In units of the longitude axis. |
| " CS[ellipsoidal, 2],\n" + |
| " Axis[“Latitude (φ)”, NORTH, Unit[“degree”, 0.017453292519943295]],\n" + |
| " Axis[“Longitude (λ)”, EAST, Unit[“degree”, 0.017453292519943295]],\n" + |
| " Unit[“grad”, 0.015707963267948967]\n," + // Inconsistent with axis units. |
| " Id[“EPSG”, 4807]]"; |
| |
| GeographicCRS crs = parseIgnoreWarnings(GeographicCRS.class, wkt); |
| final Warnings warnings = parser.getAndClearWarnings(crs); |
| assertTrue(parser.ignoredElements.isEmpty()); |
| assertNotNull(warnings); |
| assertEquals(1, warnings.getNumMessages()); |
| |
| assertNameAndIdentifierEqual("NTF (Paris)", 4807, crs); |
| PrimeMeridian pm = crs.getDatum().getPrimeMeridian(); |
| assertEquals(Units.DEGREE, pm.getAngularUnit(), "angularUnit"); |
| assertEquals(2.33722917, pm.getGreenwichLongitude(), "greenwichLongitude"); |
| EllipsoidalCS cs = crs.getCoordinateSystem(); |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertAxisEquals(AxisNames.GEODETIC_LATITUDE, "φ", AxisDirection.NORTH, -90, +90, Units.DEGREE, RangeMeaning.EXACT, cs.getAxis(0)); |
| assertAxisEquals(AxisNames.GEODETIC_LONGITUDE, "λ", AxisDirection.EAST, -180, +180, Units.DEGREE, RangeMeaning.WRAPAROUND, cs.getAxis(1)); |
| } |
| |
| /** |
| * Implementation of {@link #testGeographicCRS()} and related test methods. |
| * This test expects no {@code AUTHORITY} element on any component. |
| * |
| * @param swap 1 if axes are expected to be swapped, or 0 otherwise. |
| */ |
| private void verifyGeographicCRS(final int swap, final GeographicCRS crs) throws ParseException { |
| assertNameAndIdentifierEqual("WGS 84", 0, crs); |
| |
| final GeodeticDatum datum = crs.getDatum(); |
| assertNameAndIdentifierEqual("World Geodetic System 1984", 0, datum); |
| assertNameAndIdentifierEqual("Greenwich", 0, datum.getPrimeMeridian()); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("WGS84", 0, ellipsoid); |
| assertEquals(6378137, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(298.257223563, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| final EllipsoidalCS cs = crs.getCoordinateSystem(); |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertLongitudeAxisEquals(cs.getAxis(0 ^ swap)); |
| assertLatitudeAxisEquals (cs.getAxis(1 ^ swap)); |
| } |
| |
| /** |
| * Verifies that the axes of the coordinate system are those of a projected CRS, |
| * with (East, North) axis directions. |
| */ |
| private static void verifyProjectedCS(final CartesianCS cs, final Unit<Length> unit) { |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertUnboundedAxisEquals("Easting", "E", AxisDirection.EAST, unit, cs.getAxis(0)); |
| assertUnboundedAxisEquals("Northing", "N", AxisDirection.NORTH, unit, cs.getAxis(1)); |
| } |
| |
| /** |
| * Tests the parsing of a projected CRS from a WKT 1 string. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testProjectedCRS() throws ParseException { |
| final ProjectedCRS crs = parse(ProjectedCRS.class, |
| "PROJCS[“Mercator test”,\n" + |
| " GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST],\n" + |
| " AXIS[“Latitude”, NORTH]],\n" + |
| " PROJECTION[“Mercator_1SP”],\n" + |
| " PARAMETER[“central_meridian”, -20.0],\n" + |
| " PARAMETER[“scale_factor”, 1.0],\n" + |
| " PARAMETER[“false_easting”, 500000.0],\n" + |
| " PARAMETER[“false_northing”, 0.0],\n" + |
| " UNIT[“metre”, 1.0],\n" + |
| " AXIS[“Easting”, EAST],\n" + |
| " AXIS[“Northing”, NORTH]]"); |
| |
| assertNameAndIdentifierEqual("Mercator test", 0, crs); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.METRE); |
| verifyGeographicCRS(0, assertInstanceOf(GeographicCRS.class, crs.getBaseCRS())); |
| |
| final GeodeticDatum datum = crs.getDatum(); |
| assertNameAndIdentifierEqual("World Geodetic System 1984", 0, datum); |
| assertNameAndIdentifierEqual("Greenwich", 0, datum.getPrimeMeridian()); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("WGS84", 0, ellipsoid); |
| assertEquals(6378137, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(298.257223563, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| assertEquals("Mercator (variant A)", crs.getConversionFromBase().getMethod().getName().getCode()); |
| final ParameterValueGroup param = crs.getConversionFromBase().getParameterValues(); |
| assertEquals(6378137.0, param.parameter("semi_major" ).doubleValue(Units.METRE)); |
| assertEquals(6356752.3, param.parameter("semi_minor" ).doubleValue(Units.METRE), 0.1); |
| assertEquals( -20.0, param.parameter("central_meridian").doubleValue(Units.DEGREE)); |
| assertEquals( 1.0, param.parameter("scale_factor" ).doubleValue(Units.UNITY)); |
| assertEquals( 500000.0, param.parameter("false_easting" ).doubleValue(Units.METRE)); |
| assertEquals( 0.0, param.parameter("false_northing" ).doubleValue(Units.METRE)); |
| } |
| |
| /** |
| * Tests the parsing of a projected CRS from a WKT 1 string with authority and Bursa-Wolf parameters. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testProjectedWithID() throws ParseException { |
| final ProjectedCRS crs = parse(ProjectedCRS.class, |
| "PROJCS[“OSGB 1936 / British National Grid”,\n" + |
| " GEOGCS[“OSGB 1936”,\n" + |
| " DATUM[“OSGB_1936”,\n" + |
| " SPHEROID[“Airy 1830”, 6377563.396, 299.3249646, AUTHORITY[“EPSG”, “7001”]],\n" + |
| " TOWGS84[375.0, -111.0, 431.0, 0.0, 0.0, 0.0, 0.0],\n" + |
| " AUTHORITY[“EPSG”, “6277”]],\n" + |
| " PRIMEM[“Greenwich”,0.0, AUTHORITY[“EPSG”, “8901”]],\n" + |
| " UNIT[“DMSH”,0.0174532925199433],\n" + |
| " AXIS[“Lat”,NORTH],AXIS[“Long”,EAST], AUTHORITY[“EPSG”, “4277”]],\n" + |
| " PROJECTION[“Transverse_Mercator”],\n" + |
| " PARAMETER[“latitude_of_origin”, 49.0],\n" + |
| " PARAMETER[“central_meridian”, -2.0],\n" + |
| " PARAMETER[“scale_factor”, 0.999601272],\n" + |
| " PARAMETER[“false_easting”, 400000.0],\n" + |
| " PARAMETER[“false_northing”, -100000.0],\n" + |
| " UNIT[“metre”, 1.0, AUTHORITY[“EPSG”, “9001”]],\n" + |
| " AXIS[“E”,EAST],\n" + |
| " AXIS[“N”,NORTH],\n" + |
| " AUTHORITY[“EPSG”, “27700”]]"); |
| |
| assertNameAndIdentifierEqual("OSGB 1936 / British National Grid", 27700, crs); |
| assertNameAndIdentifierEqual("OSGB 1936", 4277, crs.getBaseCRS()); |
| assertNameAndIdentifierEqual("OSGB_1936", 6277, crs.getDatum()); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.METRE); |
| |
| final ParameterValueGroup param = crs.getConversionFromBase().getParameterValues(); |
| assertEquals("Transverse Mercator", crs.getConversionFromBase().getMethod().getName().getCode()); |
| assertEquals(6377563.396, param.parameter("semi_major" ).doubleValue(), 1E-4); |
| assertEquals(6356256.909, param.parameter("semi_minor" ).doubleValue(), 1E-3); |
| assertEquals( 49.0, param.parameter("latitude_of_origin").doubleValue(), 1E-8); |
| assertEquals( -2.0, param.parameter("central_meridian" ).doubleValue(), 1E-8); |
| assertEquals( 0.9996, param.parameter("scale_factor" ).doubleValue(), 1E-5); |
| assertEquals( 400000.0, param.parameter("false_easting" ).doubleValue(), 1E-4); |
| assertEquals( -100000.0, param.parameter("false_northing" ).doubleValue(), 1E-4); |
| |
| final BursaWolfParameters[] bwp = ((DefaultGeodeticDatum) crs.getDatum()).getBursaWolfParameters(); |
| assertEquals(1, bwp.length, "BursaWolfParameters"); |
| assertArrayEquals(new double[] {375, -111, 431}, bwp[0].getValues(), "BursaWolfParameters"); |
| } |
| |
| /** |
| * Tests the parsing of a projected CRS with feet units. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testProjectedWithFeetUnits() throws ParseException { |
| final ProjectedCRS crs = parse(ProjectedCRS.class, |
| "PROJCS[“TransverseMercator”,\n" + |
| " GEOGCS[“Sphere”,\n" + |
| " DATUM[“Sphere”,\n" + |
| " SPHEROID[“Sphere”, 6370997.0, 0.0],\n" + |
| " TOWGS84[0, 0, 0, 0, 0, 0, 0]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST],\n" + |
| " AXIS[“Latitude”, NORTH]],\n" + |
| " PROJECTION[“Transverse_Mercator”,\n" + |
| " AUTHORITY[“OGC”, “Transverse_Mercator”]],\n" + |
| " PARAMETER[“central_meridian”, 170.0],\n" + |
| " PARAMETER[“latitude_of_origin”, 50.0],\n" + |
| " PARAMETER[“scale_factor”, 0.95],\n" + |
| " PARAMETER[“false_easting”, 0.0],\n" + |
| " PARAMETER[“false_northing”, 0.0],\n" + |
| " UNIT[“US survey foot”, 0.304800609601219],\n" + |
| " AXIS[“E”, EAST],\n" + |
| " AXIS[“N”, NORTH]]"); |
| |
| assertNameAndIdentifierEqual("TransverseMercator", 0, crs); |
| assertNameAndIdentifierEqual("Sphere", 0, crs.getBaseCRS()); |
| assertNameAndIdentifierEqual("Sphere", 0, crs.getDatum()); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.US_SURVEY_FOOT); |
| |
| final ParameterValueGroup param = crs.getConversionFromBase().getParameterValues(); |
| assertEquals("Transverse Mercator", crs.getConversionFromBase().getMethod().getName().getCode()); |
| assertEquals(6370997.0, param.parameter("semi_major" ).doubleValue(), 1E-5); |
| assertEquals(6370997.0, param.parameter("semi_minor" ).doubleValue(), 1E-5); |
| assertEquals( 50.0, param.parameter("latitude_of_origin").doubleValue(), 1E-8); |
| assertEquals( 170.0, param.parameter("central_meridian" ).doubleValue(), 1E-8); |
| assertEquals( 0.95, param.parameter("scale_factor" ).doubleValue(), 1E-8); |
| assertEquals( 0.0, param.parameter("false_easting" ).doubleValue(), 1E-8); |
| assertEquals( 0.0, param.parameter("false_northing" ).doubleValue(), 1E-8); |
| } |
| |
| /** |
| * Tests the parsing of a projected CRS using angular values in grads instead of degrees |
| * and in lengths in kilometres instead of metres. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testProjectedWithGradUnits() throws ParseException { |
| String wkt = "PROJCS[“NTF (Paris) / Lambert zone II”," + |
| " GEOGCS[“NTF (Paris)”," + |
| " DATUM[“Nouvelle Triangulation Française (Paris)”," + |
| " SPHEROID[“Clarke 1880 (IGN)”, 6378249.2, 293.4660212936269]," + |
| " TOWGS84[-168,-60,320,0,0,0,0]]," + |
| " PRIMEM[“Paris”, 2.5969213, AUTHORITY[“EPSG”, “8903”]]," + // In grads. |
| " UNIT[“grad”, 0.01570796326794897]]," + |
| " PROJECTION[“Lambert Conformal Conic (1SP)”]," + // Intentional swapping of "Conformal" and "Conic". |
| " PARAMETER[“latitude_of_origin”, 52.0]," + // In grads. |
| " PARAMETER[“scale_factor”, 0.99987742]," + |
| " PARAMETER[“false_easting”, 600.0]," + |
| " PARAMETER[“false_northing”, 2200.0]," + |
| " UNIT[“km”,1000]]"; |
| |
| validateParisFranceII(parse(ProjectedCRS.class, wkt), 0, true); |
| /* |
| * Parse again using `Convention.WKT1_COMMON_UNITS` and ignoring AXIS[…] elements. |
| * See the comment in `testGeographicWithParisMeridian` method for a discussion. |
| * The new aspect tested by this method is that the unit should be ignored |
| * for the parameters in addition to the prime meridian. |
| */ |
| wkt = wkt.replace("2.5969213", "2.33722917"); // Convert unit in prime meridian. |
| wkt = wkt.replace("52.0", "46.8"); // Convert unit in “latitude_of_origin” parameter. |
| wkt = wkt.replace("600.0", "600000"); // Convert unit in “false_easting” parameter. |
| wkt = wkt.replace("2200.0", "2200000"); // Convert unit in “false_northing” parameter. |
| newParser(Convention.WKT1_IGNORE_AXES); |
| final ProjectedCRS crs = parse(ProjectedCRS.class, wkt); |
| assertNameAndIdentifierEqual("NTF (Paris) / Lambert zone II", 0, crs); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.KILOMETRE); |
| final PrimeMeridian pm = verifyNTF(crs.getDatum(), true); |
| assertEquals(Units.DEGREE, pm.getAngularUnit(), "angularUnit"); |
| assertEquals(2.33722917, pm.getGreenwichLongitude(), "greenwichLongitude"); |
| final ParameterValue<?> param = verifyNTF(crs.getConversionFromBase().getParameterValues()); |
| assertEquals(Units.DEGREE, param.getUnit(), "angularUnit"); |
| assertEquals(46.8, param.doubleValue(), "latitude_of_origin"); |
| } |
| |
| /** |
| * Tests the same CRS as {@link #testProjectedWithGradUnits()}, but from a WKT 2 definition |
| * (except for inclusion of accented characters). |
| * |
| * @throws ParseException if the parsing failed. |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-309">SIS-309</a> |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-310">SIS-310</a> |
| */ |
| @Test |
| public void testProjectedFromWKT2() throws ParseException { |
| String wkt = "ProjectedCRS[“NTF (Paris) / Lambert zone II”,\n" + |
| " BaseGeodCRS[“NTF (Paris)”,\n" + |
| " Datum[“Nouvelle Triangulation Française (Paris)”,\n" + |
| " Ellipsoid[“Clarke 1880 (IGN)”, 6378249.2, 293.4660212936269]],\n" + |
| " PrimeMeridian[“Paris”, 2.5969213, Unit[“grad”, 0.015707963267948967], Id[“EPSG”, 8903]],\n" + |
| " AngleUnit[“degree”, 0.017453292519943295]],\n" + |
| " Conversion[“Lambert zone II”,\n" + |
| " Method[“Lambert Conic Conformal (1SP)”],\n" + |
| " Parameter[“Latitude of natural origin”, 52.0, AngleUnit[“grad”, 0.015707963267948967]],\n" + |
| " Parameter[“Longitude of natural origin”, 0.0],\n" + |
| " Parameter[“Scale factor at natural origin”, 0.99987742],\n" + |
| " Parameter[“False easting”, 600.0],\n" + |
| " Parameter[“False northing”, 2200.0]],\n" + |
| " CS[Cartesian, 2],\n" + |
| " Axis[“Easting (E)”, east],\n" + |
| " Axis[“Northing (N)”, north],\n" + |
| " LengthUnit[“kilometre”, 1000],\n" + |
| " Scope[“Large and medium scale topographic mapping and engineering survey.”],\n" + |
| " Id[“EPSG”, 27572, URI[“urn:ogc:def:crs:EPSG::27572”]]]"; |
| |
| final ProjectedCRS crs = parse(ProjectedCRS.class, wkt); |
| validateParisFranceII(crs, 27572, false); |
| assertNull(getSingleton(crs.getIdentifiers()).getVersion(), "Identifier shall not have a version."); |
| } |
| |
| /** |
| * Verifies the parameters of a “NTF (Paris) / Lambert zone II” projection. |
| */ |
| private static void validateParisFranceII(final ProjectedCRS crs, final int identifier, final boolean hasToWGS84) { |
| assertNameAndIdentifierEqual("NTF (Paris) / Lambert zone II", identifier, crs); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.KILOMETRE); |
| final PrimeMeridian pm = verifyNTF(crs.getDatum(), hasToWGS84); |
| assertEquals(Units.GRAD, pm.getAngularUnit(), "angularUnit"); |
| assertEquals(2.5969213, pm.getGreenwichLongitude(), "greenwichLongitude"); |
| final ParameterValue<?> param = verifyNTF(crs.getConversionFromBase().getParameterValues()); |
| assertEquals(Units.GRAD, param.getUnit(), "angularUnit"); |
| assertEquals(52.0, param.doubleValue(), "latitude_of_origin"); |
| } |
| |
| /** |
| * Verifies the properties of a datum which is expected to be “Nouvelle Triangulation Française (Paris)”. |
| * This is used by the methods in this class which test a CRS using less frequently used units and prime |
| * meridian. |
| * |
| * @param datum the datum to verify. |
| * @param hasToWGS84 Whether the datum is expected to have a {@code TOWGS84[…]} element. |
| * @return the prime meridian, to be verified by the caller because the unit of measurement depends on the test. |
| */ |
| private static PrimeMeridian verifyNTF(final GeodeticDatum datum, final boolean hasToWGS84) { |
| assertNameAndIdentifierEqual("Nouvelle Triangulation Française (Paris)", 0, datum); |
| |
| final BursaWolfParameters[] bwp = ((DefaultGeodeticDatum) datum).getBursaWolfParameters(); |
| assertEquals(hasToWGS84 ? 1 : 0, bwp.length, "BursaWolfParameters"); |
| if (hasToWGS84) { |
| assertArrayEquals(new double[] {-168, -60, 320}, bwp[0].getValues(), "BursaWolfParameters"); |
| } |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("Clarke 1880 (IGN)", 0, ellipsoid); |
| assertEquals(6378249.2, ellipsoid.getSemiMajorAxis(), "semiMajor"); |
| assertEquals(293.4660212936269, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| final PrimeMeridian pm = datum.getPrimeMeridian(); |
| assertNameAndIdentifierEqual("Paris", 8903, pm); |
| return pm; |
| } |
| |
| /** |
| * Verifies the parameter values for a projected CRS which is expected to be “NTF (Paris) / Lambert zone II”. |
| * This is used by the methods in this class which test a CRS using less frequently used units and prime meridian. |
| * |
| * @return the latitude of origin, to be verified by the caller because the unit of measurement depends on the test. |
| */ |
| private static ParameterValue<?> verifyNTF(final ParameterValueGroup param) { |
| assertEquals("Lambert Conic Conformal (1SP)", param.getDescriptor().getName().getCode()); |
| assertEquals( 6378249.2, param.parameter("semi_major" ).doubleValue(Units.METRE)); |
| assertEquals( 6356515.0, param.parameter("semi_minor" ).doubleValue(Units.METRE), 1E-12); |
| assertEquals( 0.0, param.parameter("central_meridian").doubleValue(Units.DEGREE)); |
| assertEquals(0.99987742, param.parameter("scale_factor" ).doubleValue(Units.UNITY)); |
| assertEquals( 600000.0, param.parameter("false_easting" ).doubleValue(Units.METRE)); |
| assertEquals( 2200000.0, param.parameter("false_northing" ).doubleValue(Units.METRE)); |
| return param.parameter("latitude_of_origin"); |
| } |
| |
| /** |
| * Tests parsing a WKT with a missing Geographic CRS name. |
| * This should be considered invalid, but happen in practice. |
| * |
| * <p>The WKT tested in this method also contains some other oddities compared to the usual WKT:</p> |
| * <ul> |
| * <li>The prime meridian is declared in the {@code "central_meridian"} projection parameter instead |
| * than in the {@code PRIMEM[…]} element.</li> |
| * <li>Some elements are not in the usual order.</li> |
| * </ul> |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testProjectedWithMissingName() throws ParseException { |
| final ProjectedCRS crs = parse(ProjectedCRS.class, |
| "PROJCS[“FRANCE/NTF/Lambert III”," + |
| "GEOGCS[“”," + // Missing name (the purpose of this test). |
| "DATUM[“NTF=GR3DF97A”,TOWGS84[-168, -60, 320] ," + // Intentionally misplaced coma. |
| "SPHEROID[“Clarke 1880 (IGN)”,6378249.2,293.4660212936269]]," + |
| "PRIMEM[“Greenwich”,0],UNIT[“Degrees”,0.0174532925199433]," + |
| "AXIS[“Long”,East],AXIS[“Lat”,North]]," + |
| "PROJECTION[“Lambert_Conformal_Conic_1SP”]," + |
| "PARAMETER[“latitude_of_origin”,44.1]," + |
| "PARAMETER[“central_meridian”,2.33722917]," + // Paris prime meridian. |
| "PARAMETER[“scale_factor”,0.999877499]," + |
| "PARAMETER[“false_easting”,600000]," + |
| "PARAMETER[“false_northing”,200000]," + |
| "UNIT[“Meter”,1]," + |
| "AXIS[“Easting”,East],AXIS[“Northing”,North]]"); |
| |
| assertNameAndIdentifierEqual("FRANCE/NTF/Lambert III", 0, crs); |
| verifyProjectedCS(crs.getCoordinateSystem(), Units.METRE); |
| final GeographicCRS geoCRS = assertInstanceOf(GeographicCRS.class, crs.getBaseCRS()); |
| assertNameAndIdentifierEqual("NTF=GR3DF97A", 0, geoCRS); // Inherited the datum name. |
| |
| final GeodeticDatum datum = geoCRS.getDatum(); |
| assertNameAndIdentifierEqual("NTF=GR3DF97A", 0, datum); |
| assertNameAndIdentifierEqual("Greenwich", 0, datum.getPrimeMeridian()); |
| assertArrayEquals(new double[] {-168, -60, 320}, |
| assertInstanceOf(DefaultGeodeticDatum.class, datum).getBursaWolfParameters()[0].getValues(), |
| "BursaWolfParameters"); |
| |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| assertNameAndIdentifierEqual("Clarke 1880 (IGN)", 0, ellipsoid); |
| assertEquals(6378249.2, ellipsoid.getSemiMajorAxis(),"semiMajor"); |
| assertEquals(293.4660212936269, ellipsoid.getInverseFlattening(), "inverseFlattening"); |
| |
| final EllipsoidalCS cs = geoCRS.getCoordinateSystem(); |
| assertEquals(2, cs.getDimension(), "dimension"); |
| assertLongitudeAxisEquals(cs.getAxis(0)); |
| assertLatitudeAxisEquals (cs.getAxis(1)); |
| |
| final ParameterValueGroup param = crs.getConversionFromBase().getParameterValues(); |
| assertEquals("Lambert Conic Conformal (1SP)", param.getDescriptor().getName().getCode()); |
| assertEquals( 6378249.2, param.parameter("semi_major" ).doubleValue(Units.METRE)); |
| assertEquals( 6356515.0, param.parameter("semi_minor" ).doubleValue(Units.METRE), 1E-12); |
| assertEquals( 44.1, param.parameter("latitude_of_origin").doubleValue(Units.DEGREE)); |
| assertEquals( 2.33722917, param.parameter("central_meridian" ).doubleValue(Units.DEGREE)); |
| assertEquals(0.999877499, param.parameter("scale_factor" ).doubleValue(Units.UNITY)); |
| assertEquals( 600000.0, param.parameter("false_easting" ).doubleValue(Units.METRE)); |
| assertEquals( 200000.0, param.parameter("false_northing" ).doubleValue(Units.METRE)); |
| } |
| |
| /** |
| * Parses a test CRS north or south oriented. |
| * If the CRS is fully south-oriented with 0.0 northing, then it should be the EPSG:22285 one. |
| */ |
| private ProjectedCRS parseTransverseMercator(final boolean methodSouth, |
| final boolean axisSouth, final double northing) throws ParseException |
| { |
| final String method = methodSouth ? "Transverse Mercator (South Orientated)" : "Transverse Mercator"; |
| final String axis = axisSouth ? "“Southing”, SOUTH" : "“Northing”, NORTH"; |
| return parse(ProjectedCRS.class, |
| "PROJCS[“South African Coordinate System zone 25”, " + |
| "GEOGCS[“Cape”, " + |
| "DATUM[“Cape”, " + |
| "SPHEROID[“Clarke 1880 (Arc)”, 6378249.145, 293.4663077, AUTHORITY[“EPSG”,“7013”]], " + |
| "TOWGS84[-136.0, -108.0, -292.0], " + |
| "AUTHORITY[“EPSG”,“6222”]], " + |
| "PRIMEM[“Greenwich”, 0.0, AUTHORITY[“EPSG”,“8901”]], " + |
| "UNIT[“degree”, 0.017453292519943295], " + |
| "AXIS[“Geodetic latitude”, NORTH], " + |
| "AXIS[“Geodetic longitude”, EAST], " + |
| "AUTHORITY[“EPSG”,“4222”]], " + |
| "PROJECTION[“" + method + "”], " + |
| "PARAMETER[“central_meridian”, 25.0], " + |
| "PARAMETER[“latitude_of_origin”, 0.0], " + |
| "PARAMETER[“scale_factor”, 1.0], " + |
| "PARAMETER[“false_easting”, 0.0], " + |
| "PARAMETER[“false_northing”, " + northing + "], " + |
| "UNIT[“m”, 1.0], " + |
| "AXIS[“Westing”, WEST], " + |
| "AXIS[" + axis + "]]"); |
| } |
| |
| /** |
| * Returns the conversion from {@code north} to {@code south}. |
| */ |
| private static Matrix conversion(final ProjectedCRS north, final ProjectedCRS south) |
| throws NoninvertibleTransformException |
| { |
| final MathTransform transform = MathTransforms.concatenate( |
| north.getConversionFromBase().getMathTransform().inverse(), |
| south.getConversionFromBase().getMathTransform()); |
| return assertInstanceOf(LinearTransform.class, transform).getMatrix(); |
| } |
| |
| /** |
| * Tests the {@link MathTransform} between North-Orientated and South-Orientated cases. |
| * |
| * @throws ParseException if the parsing failed. |
| * @throws NoninvertibleTransformException if computation of the conversion from North-Orientated |
| * to South-Orientated failed. |
| */ |
| @Test |
| public void testMathTransform() throws ParseException, NoninvertibleTransformException { |
| /* |
| * Test "Transverse Mercator" (not south-oriented) with an axis oriented toward south. |
| * The `south` transform is actually the usual Transverse Mercator projection, despite |
| * having axis oriented toward South. Consequently, the "False Northing" parameter has |
| * the same meaning for those two CRS. Since we assigned the same False Northing value, |
| * those two CRS have their "False origin" at the same location. This is why conversion |
| * from `south` to `north` introduce no translation, only a reversal of y axis. |
| */ |
| ProjectedCRS north = parseTransverseMercator(false, false, 1000); |
| assertEquals(AxisDirection.WEST, north.getCoordinateSystem().getAxis(0).getDirection()); |
| assertEquals(AxisDirection.NORTH, north.getCoordinateSystem().getAxis(1).getDirection()); |
| |
| ProjectedCRS south = parseTransverseMercator(false, true, 1000); |
| assertEquals(AxisDirection.WEST, south.getCoordinateSystem().getAxis(0).getDirection()); |
| assertEquals(AxisDirection.SOUTH, south.getCoordinateSystem().getAxis(1).getDirection()); |
| |
| Matrix matrix = conversion(north, south); |
| assertEquals(+1, matrix.getElement(0,0), "West direction should be unchanged. "); |
| assertEquals(-1, matrix.getElement(1,1), "North-South direction should be reverted."); |
| assertEquals( 0, matrix.getElement(0,2), "No easting expected."); |
| assertEquals( 0, matrix.getElement(1,2), "No northing expected."); |
| assertDiagonalEquals(new double[] {+1, -1, 1}, true, matrix, STRICT); |
| /* |
| * Test "Transverse Mercator South Orientated". In this projection, the "False Northing" parameter |
| * is actually a "False Southing". It may sound surprising, but "South Orientated" projections are |
| * defined that way. For converting from our CRS having a False Northing of 1000 to a CRS without |
| * False Northing or Southing, we must subtract 1000 from the axis which is oriented toward North. |
| * This means adding 1000 if the axis is rather oriented toward South. Then we add another 1000 m |
| * (the value specified in the line just below) toward South. |
| */ |
| south = parseTransverseMercator(true, true, 1000); // "False Southing" of 1000 metres. |
| assertEquals(AxisDirection.WEST, south.getCoordinateSystem().getAxis(0).getDirection()); |
| assertEquals(AxisDirection.SOUTH, south.getCoordinateSystem().getAxis(1).getDirection()); |
| matrix = conversion(north, south); |
| assertEquals( +1, matrix.getElement(0,0), "West direction should be unchanged."); |
| assertEquals( -1, matrix.getElement(1,1), "North-South direction should be reverted."); |
| assertEquals( 0, matrix.getElement(0,2), "No easting expected."); |
| assertEquals(2000, matrix.getElement(1,2), "Northing expected."); |
| } |
| |
| /** |
| * Tests the parsing of a derived CRS from a WKT 2 string. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testDerivedCRS() throws ParseException { |
| final DerivedCRS crs = parse(DerivedCRS.class, |
| "GeodCRS[“EPSG topocentric example B”,\n" + |
| " BaseGeodCRS[“WGS 84”,\n" + |
| " Datum[“World Geodetic System 1984”,\n" + |
| " Ellipsoid[“WGS 84”, 6378137.0, 298.257223563, LengthUnit[“metre”, 1]]],\n" + |
| " PrimeM[“Greenwich”, 0.0, AngleUnit[“degree”, 0.017453292519943295]]],\n" + |
| " DerivingConversion[“EPSG topocentric example B”,\n" + |
| " Method[“Geocentric/topocentric conversions”, Id[“EPSG”, 9836]],\n" + |
| " Parameter[“Geocentric X of topocentric origin”, 3771793.97, LengthUnit[“metre”, 1], Id[“EPSG”, 8837]],\n" + |
| " Parameter[“Geocentric Y of topocentric origin”, 140253.34, LengthUnit[“metre”, 1], Id[“EPSG”, 8838]],\n" + |
| " Parameter[“Geocentric Z of topocentric origin”, 5124304.35, LengthUnit[“metre”, 1], Id[“EPSG”, 8839]]],\n" + |
| " CS[Cartesian, 3],\n" + |
| " Axis[“Topocentric East (U)”, east],\n" + |
| " Axis[“Topocentric North (V)”, north],\n" + |
| " Axis[“Topocentric height (W)”, up],\n" + |
| " LengthUnit[“metre”, 1],\n" + |
| " Scope[“Example only - fictitious.”],\n" + |
| " Id[“EPSG”, 5820, “9.9.1”, URI[“urn:ogc:def:crs:EPSG:9.9.1:5820”]]]"); |
| |
| assertNameAndIdentifierEqual("EPSG topocentric example B", 5820, crs); |
| assertNameAndIdentifierEqual("EPSG topocentric example B", 0, crs.getConversionFromBase()); |
| CoordinateSystem cs = crs.getCoordinateSystem(); |
| assertInstanceOf(CartesianCS.class, cs, "coordinateSystem"); |
| assertEquals(3, cs.getDimension(), "dimension"); |
| assertUnboundedAxisEquals("Topocentric East", "U", AxisDirection.EAST, Units.METRE, cs.getAxis(0)); |
| assertUnboundedAxisEquals("Topocentric North", "V", AxisDirection.NORTH, Units.METRE, cs.getAxis(1)); |
| assertUnboundedAxisEquals("Topocentric height", "W", AxisDirection.UP, Units.METRE, cs.getAxis(2)); |
| /* |
| * The type of the coordinate system of the base CRS is not specified in the WKT. |
| * The parser should use the `AbstractProvider.sourceCSType` field for detecting |
| * that the expected type for “Geocentric/topocentric conversions” is Cartesian. |
| */ |
| cs = crs.getBaseCRS().getCoordinateSystem(); |
| assertInstanceOf(CartesianCS.class, cs, "coordinateSystem"); |
| assertEquals(3, cs.getDimension(), "dimension"); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_X, "X", AxisDirection.GEOCENTRIC_X, Units.METRE, cs.getAxis(0)); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Y, "Y", AxisDirection.GEOCENTRIC_Y, Units.METRE, cs.getAxis(1)); |
| assertUnboundedAxisEquals(AxisNames.GEOCENTRIC_Z, "Z", AxisDirection.GEOCENTRIC_Z, Units.METRE, cs.getAxis(2)); |
| } |
| |
| /** |
| * Tests the parsing of an engineering CRS from a WKT 2 string. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testEngineeringCRS() throws ParseException { |
| final EngineeringCRS crs = parse(EngineeringCRS.class, |
| "EngineeringCRS[“A building-centred CRS”,\n" + |
| " EngineeringDatum[“Building reference point”],\n" + |
| " CS[Cartesian, 3],\n" + |
| " Axis[“x”, east],\n" + |
| " Axis[“y”, north],\n" + |
| " Axis[“z”, up],\n" + |
| " Unit[“metre”, 1]]"); |
| |
| assertNameAndIdentifierEqual("A building-centred CRS", 0, crs); |
| assertNameAndIdentifierEqual("Building reference point", 0, crs.getDatum()); |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| assertInstanceOf(CartesianCS.class, cs, "coordinateSystem"); |
| assertEquals(3, cs.getDimension(), "dimension"); |
| |
| // Axis names are arbitrary and could change in future SIS versions. |
| assertUnboundedAxisEquals("Easting", "x", AxisDirection.EAST, Units.METRE, cs.getAxis(0)); |
| assertUnboundedAxisEquals("Northing", "y", AxisDirection.NORTH, Units.METRE, cs.getAxis(1)); |
| assertUnboundedAxisEquals("z", "z", AxisDirection.UP, Units.METRE, cs.getAxis(2)); |
| } |
| |
| /** |
| * Tests the parsing of a compound CRS from a WKT 1 string, except the time dimension which is WKT 2. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testCompoundCRS() throws ParseException { |
| final CompoundCRS crs = parse(CompoundCRS.class, |
| "COMPD_CS[“WGS 84 + height + time”,\n" + |
| " GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST],\n" + |
| " AXIS[“Latitude”, NORTH]],\n" + |
| " VERT_CS[“Gravity-related height”,\n" + |
| " VERT_DATUM[“Mean Sea Level”, 2005],\n" + |
| " UNIT[“metre”, 1],\n" + |
| " AXIS[“Gravity-related height”, UP]],\n" + |
| " TimeCRS[“Time”,\n" + // WKT 2 |
| " TimeDatum[“Modified Julian”, TimeOrigin[1858-11-17T00:00:00.0Z]],\n" + |
| " Unit[“day”, 86400],\n" + |
| " Axis[“Time”, FUTURE]]]"); |
| |
| // CompoundCRS parent |
| assertNameAndIdentifierEqual("WGS 84 + height + time", 0, crs); |
| final Iterator<CoordinateReferenceSystem> components = crs.getComponents().iterator(); |
| |
| // GeographicCRS child |
| verifyGeographicCRS(0, (GeographicCRS) components.next()); |
| |
| // VerticalCRS child |
| final VerticalCRS vertCRS = (VerticalCRS) components.next(); |
| assertNameAndIdentifierEqual("Gravity-related height", 0, vertCRS); |
| assertNameAndIdentifierEqual("Mean Sea Level", 0, vertCRS.getDatum()); |
| |
| // TemporalCRS child |
| final TemporalCRS timeCRS = (TemporalCRS) components.next(); |
| final TemporalDatum timeDatum = timeCRS.getDatum(); |
| assertNameAndIdentifierEqual("Time", 0, timeCRS); |
| assertNameAndIdentifierEqual("Modified Julian", 0, timeDatum); |
| assertEquals(Instant.ofEpochSecond(-40587L * SECONDS_PER_DAY), timeDatum.getOrigin(), "epoch"); |
| |
| // No more CRS. |
| assertFalse(components.hasNext()); |
| |
| // Axes: we verify only the CompoundCRS ones, which should include all others. |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| assertEquals(4, cs.getDimension(), "dimension"); |
| assertLongitudeAxisEquals(cs.getAxis(0)); |
| assertLatitudeAxisEquals (cs.getAxis(1)); |
| assertUnboundedAxisEquals("Gravity-related height", "H", AxisDirection.UP, Units.METRE, cs.getAxis(2)); |
| assertUnboundedAxisEquals("Time", "t", AxisDirection.FUTURE, Units.DAY, cs.getAxis(3)); |
| } |
| |
| /** |
| * Tests the parsing of a compound CRS from a WKT 1 string with ellipsoidal height and its conversion |
| * to a three-dimensional geographic CRS. |
| * |
| * @throws ParseException if the parsing failed. |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-317">SIS-317</a> |
| */ |
| @Test |
| public void testCompoundWKT1() throws ParseException { |
| final GeographicCRS crs = parse(GeographicCRS.class, |
| "COMPD_CS[“WGS 84 (3D)”,\n" + |
| " GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0],\n" + |
| " UNIT[“degree”, 0.017453292519943295],\n" + |
| " AXIS[“Longitude”, EAST],\n" + |
| " AXIS[“Latitude”, NORTH]],\n" + |
| " VERT_CS[“Ellipsoidal height”,\n" + |
| " VERT_DATUM[“Ellipsoid”, 2002],\n" + |
| " UNIT[“metre”, 1],\n" + |
| " AXIS[“Ellipsoidal height”, UP]]]"); |
| |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| assertEquals(3, cs.getDimension(), "dimension"); |
| assertLongitudeAxisEquals(cs.getAxis(0)); |
| assertLatitudeAxisEquals (cs.getAxis(1)); |
| assertUnboundedAxisEquals("Ellipsoidal height", "h", AxisDirection.UP, Units.METRE, cs.getAxis(2)); |
| } |
| |
| /** |
| * Tests the parsing of a {@code GEOGTRAN} coordinate operation. |
| * This is specific to ESRI. |
| * |
| * @throws ParseException if the parsing failed. |
| * |
| * @see <a href="https://issues.apache.org/jira/browse/SIS-538">SIS-538</a> |
| */ |
| @Test |
| public void testGeogTran() throws ParseException { |
| final CoordinateOperation op = parse(CoordinateOperation.class, |
| "GEOGTRAN[“Palestine_1923_to_WGS_84_1”,\n" + |
| " GEOGCS[“GCS_Palestine_1923”,\n" + |
| " DATUM[“D_Palestine_1923”,\n" + |
| " SPHEROID[“Clarke_1880_Benoit”, 6378300.789, 293.46631553898]],\n" + |
| " PRIMEM[“Greenwich”, 0.0], UNIT[“Degree”, 0.0174532925199433]],\n" + |
| " GEOGCS[“GCS_WGS_1984”,\n" + |
| " DATUM[“D_WGS_1984”,\n" + |
| " SPHEROID[“WGS_1984”, 6378137.0, 298.257223563]],\n" + |
| " PRIMEM[“Greenwich”, 0.0], UNIT[“Degree”, 0.0174532925199433]],\n" + |
| " METHOD[“Position_Vector”],\n" + |
| " PARAMETER[“X_Axis_Translation”, -275.7224],\n" + |
| " PARAMETER[“Y_Axis_Translation”, 94.7824],\n" + |
| " PARAMETER[“Z_Axis_Translation”, 340.8944],\n" + |
| " PARAMETER[“X_Axis_Rotation”, -8.001],\n" + |
| " PARAMETER[“Y_Axis_Rotation”, -4.42],\n" + |
| " PARAMETER[“Z_Axis_Rotation”, -11.821],\n" + |
| " PARAMETER[“Scale_Difference”, 1.0],\n" + |
| " AUTHORITY[“EPSG”, 1074]]"); |
| |
| final GeographicCRS sourceCRS = (GeographicCRS) op.getSourceCRS(); |
| final GeographicCRS targetCRS = (GeographicCRS) op.getTargetCRS(); |
| assertNameAndIdentifierEqual("GCS_Palestine_1923", 0, sourceCRS); |
| assertNameAndIdentifierEqual("GCS_WGS_1984", 0, targetCRS); |
| } |
| |
| /** |
| * Ensures that parsing a WKT with wrong units throws an exception. |
| */ |
| @Test |
| public void testIncompatibleUnits() { |
| var exception = assertThrows(ParseException.class, () -> parse(GeographicCRS.class, |
| "GEOGCS[“NAD83”,\n" + |
| " DATUM[“North American Datum 1983”,\n" + |
| " SPHEROID[“GRS 1980”, 6378137.0, 298.257222]],\n" + |
| " PRIMEM[“Greenwich”, 0],\n" + |
| " UNIT[“kilometre”, 1000]]")); // Wrong unit); |
| |
| assertMessageContains(exception, "km"); |
| } |
| |
| /** |
| * Tests the production of a warning messages when the WKT contains unknown elements. |
| * |
| * @throws ParseException if the parsing failed. |
| */ |
| @Test |
| public void testWarnings() throws ParseException { |
| final GeographicCRS crs = parseIgnoreWarnings(GeographicCRS.class, |
| "GEOGCS[“WGS 84”,\n" + |
| " DATUM[“World Geodetic System 1984”,\n" + |
| " SPHEROID[“WGS84”, 6378137.0, 298.257223563, Ext1[“foo”], Ext2[“bla”]]],\n" + |
| " PRIMEM[“Greenwich”, 0.0, Intruder[“unknown”], UNIT[“degree”, 0.01746]],\n" + // Inaccurate scale factor. |
| " UNIT[“degree”, 0.017453292519943295], Intruder[“foo”]]"); |
| |
| verifyGeographicCRS(0, crs); |
| final Warnings warnings = parser.getAndClearWarnings(crs); |
| assertNotNull(warnings, "warnings"); |
| assertTrue(warnings.getExceptions().isEmpty()); |
| assertEquals("WGS 84", warnings.getRootElement()); |
| assertArrayEquals(new String[] {"Intruder", "Ext1", "Ext2"}, warnings.getUnknownElements().toArray()); |
| assertArrayEquals(new String[] {"PRIMEM", "GEOGCS"}, warnings.getUnknownElementLocations("Intruder").toArray()); |
| assertArrayEquals(new String[] {"SPHEROID"}, warnings.getUnknownElementLocations("Ext1").toArray()); |
| assertArrayEquals(new String[] {"SPHEROID"}, warnings.getUnknownElementLocations("Ext2").toArray()); |
| |
| assertMultilinesEquals("Parsing of “WGS 84” done, but some elements were ignored.\n" + |
| " • Unexpected scale factor 0.01746 for unit of measurement “°”.\n" + |
| " • The text contains unknown elements:\n" + |
| " ‣ “Intruder” in PRIMEM, GEOGCS.\n" + |
| " ‣ “Ext1” in SPHEROID.\n" + |
| " ‣ “Ext2” in SPHEROID.", warnings.toString(Locale.US)); |
| |
| assertMultilinesEquals("La lecture de « WGS 84 » a été faite, mais en ignorant certains éléments.\n" + |
| " • Le facteur d’échelle 0,01746 est inattendu pour l’unité de mesure « ° ».\n" + |
| " • Le texte contient des éléments inconnus :\n" + |
| " ‣ « Intruder » dans PRIMEM, GEOGCS.\n" + |
| " ‣ « Ext1 » dans SPHEROID.\n" + |
| " ‣ « Ext2 » dans SPHEROID.", warnings.toString(Locale.FRANCE)); |
| } |
| } |