| /* |
| * 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.Set; |
| import java.util.Map; |
| import java.util.List; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.function.Consumer; |
| import java.util.function.BiFunction; |
| import java.util.stream.Stream; |
| import java.io.BufferedReader; |
| import java.io.InputStreamReader; |
| import java.io.IOException; |
| import org.opengis.util.FactoryException; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.NoSuchAuthorityCodeException; |
| import org.opengis.referencing.crs.SingleCRS; |
| import org.opengis.referencing.crs.ProjectedCRS; |
| import org.opengis.referencing.crs.GeographicCRS; |
| import org.opengis.referencing.crs.GeodeticCRS; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.apache.sis.metadata.iso.DefaultIdentifier; |
| |
| // Test dependencies |
| import org.junit.jupiter.api.Test; |
| import static org.junit.jupiter.api.Assertions.*; |
| import org.apache.sis.test.TestUtilities; |
| import org.apache.sis.test.TestCase; |
| import static org.apache.sis.test.Assertions.assertSetEquals; |
| import static org.apache.sis.test.Assertions.assertMessageContains; |
| |
| // Specific to the geoapi-3.1 and geoapi-4.0 branches: |
| import org.opengis.metadata.Identifier; |
| import static org.opengis.test.Assertions.assertAxisDirectionsEqual; |
| |
| |
| /** |
| * Tests {@link WKTDictionary}. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| public final class WKTDictionaryTest extends TestCase { |
| /** |
| * Creates a new test case. |
| */ |
| public WKTDictionaryTest() { |
| } |
| |
| /** |
| * Tests {@link WKTDictionary#addDefinitions(Stream)}. The CRS used in this test are a subset of the |
| * ones used by {@link #testLoad()}. One of them is intentionally malformed for testing error index. |
| * |
| * @throws FactoryException if an error occurred while parsing a WKT. |
| */ |
| @Test |
| public void testAddDefinitions() throws FactoryException { |
| final WKTDictionary factory = new WKTDictionary(null); |
| factory.addDefinitions(List.of( |
| "GeodCRS[\"Anguilla 1957\",\n" + |
| " Datum[\"Anguilla 1957\",\n" + |
| " Ellipsoid[\"Clarke 1880\", 6378249.145, 293.465]],\n" + |
| " CS[ellipsoidal, 2],\n" + |
| " Axis[\"Latitude\", north],\n" + |
| " Axis[\"Longitude\", east],\n" + |
| " Unit[\"Degree\", 0.0174532925199433],\n" + |
| " Id[\"TEST\", 21]]", |
| |
| "GeodCRS[\"Error index 69 (on Ellipsoid)\", Datum[\"Erroneous\", Ellipsoid[\"Missing axis length\"]],\n" + |
| " CS[ellipsoidal, 2],\n" + |
| " Axis[\"Latitude\", north],\n" + |
| " Axis[\"Longitude\", east],\n" + |
| " Unit[\"Degree\", 0.0174532925199433],\n" + |
| " Id[\"TEST\", \"E1\"]]").stream()); |
| /* |
| * Codes can be in any order. Code spaces are omitted when there is no ambiguity. |
| */ |
| assertArrayEquals(new String[] {"TEST"}, factory.getCodeSpaces().toArray(), "getCodeSpaces()"); |
| assertEquals("TEST", factory.getAuthority().getTitle().toString(), "getAuthority()"); |
| Set<String> codes = factory.getAuthorityCodes(IdentifiedObject.class); |
| assertSame( codes, factory.getAuthorityCodes(SingleCRS.class)); |
| assertSame( codes, factory.getAuthorityCodes(GeodeticCRS.class)); |
| assertSame( codes, factory.getAuthorityCodes(GeographicCRS.class)); |
| assertEquals(0, factory.getAuthorityCodes(ProjectedCRS.class).size()); |
| assertSetEquals(List.of("21", "E1"), codes); |
| /* |
| * Tests CRS creation, potentially with expected error. |
| */ |
| verifyCRS(factory.createGeographicCRS("21"), "Anguilla 1957"); |
| verifyErroneousCRS(factory, "E1", 69); |
| /* |
| * Test non-existing CRS. |
| */ |
| var exception = assertThrows(NoSuchAuthorityCodeException.class, () -> factory.createGeographicCRS("84")); |
| assertMessageContains(exception, "84"); |
| } |
| |
| /** |
| * Tests {@link WKTDictionary#load(BufferedReader)}. |
| * |
| * @throws IOException if an error occurred while reading the test file. |
| * @throws FactoryException if an error occurred while parsing a WKT. |
| */ |
| @Test |
| public void testLoad() throws IOException, FactoryException { |
| final WKTDictionary factory = new WKTDictionary(null); |
| try (BufferedReader source = new BufferedReader(new InputStreamReader( |
| WKTFormatTest.class.getResourceAsStream("ExtraCRS.txt"), "UTF-8"))) |
| { |
| factory.load(source); |
| } |
| /* |
| * The `load(…)` method should detect duplicated WKT elements and use references |
| * to unique instances of nodes such as "AngleUnit["Degree", 0.0174532925199433]]". |
| * Following line verifies if the trees of WKT elements indeed share common nodes. |
| */ |
| new SharedValuesCheck().verify(factory); |
| /* |
| * "TEST" code space should be first because it is the most frequently used |
| * in the test file. The authority should be "TEST" for the same reason. |
| * Codes can be in any order. Code spaces are omitted when there is no ambiguity. |
| */ |
| assertArrayEquals(new String[] {"TEST", "ESRI"}, factory.getCodeSpaces().toArray(), "getCodeSpaces()"); |
| assertEquals("TEST", factory.getAuthority().getTitle().toString(), "getAuthority()"); |
| Set<String> codes = factory.getAuthorityCodes(IdentifiedObject.class); |
| assertSame( codes, factory.getAuthorityCodes(IdentifiedObject.class)); // Test caching. |
| assertSame( codes, factory.getAuthorityCodes(SingleCRS.class)); // Test sharing. |
| assertSetEquals(List.of("102018", "ESRI::102021", "TEST::102021", "TEST:v2:102021", "E1", "E2"), codes); |
| assertSetEquals(List.of("102018", "ESRI::102021"), factory.getAuthorityCodes(ProjectedCRS.class)); |
| codes = factory.getAuthorityCodes(GeographicCRS.class); |
| assertSetEquals(List.of("TEST::102021", "TEST:v2:102021", "E1", "E2"), codes); |
| assertSame(codes, factory.getAuthorityCodes(GeodeticCRS.class)); // Test sharing. |
| assertSame(codes, factory.getAuthorityCodes(GeographicCRS.class)); // Test caching. |
| /* |
| * Test descriptions before CRS creation. |
| * Implementation fetches them from `StoredTree` instances. |
| */ |
| assertDescriptionEquals("North_Pole_Stereographic", factory, "ESRI::102018"); |
| assertDescriptionEquals("South_Pole_Stereographic", factory, "ESRI::102021"); |
| /* |
| * Tests CRS creation. |
| */ |
| verifyCRS(factory.createProjectedCRS ( "102018"), "North_Pole_Stereographic", +90); |
| verifyCRS(factory.createProjectedCRS ("ESRI : 102021"), "South_Pole_Stereographic", -90); |
| verifyCRS(factory.createGeographicCRS("TEST: :102021"), "Anguilla 1957"); |
| verifyCRS(factory.createGeographicCRS("TEST:v2:102021"), "Anguilla 1957 (bis)"); |
| /* |
| * Test descriptions after CRS creation. |
| * Implementation fetches them from `IdentifiedObject` instances. |
| */ |
| assertDescriptionEquals("North_Pole_Stereographic", factory, "ESRI::102018"); |
| assertDescriptionEquals("South_Pole_Stereographic", factory, "ESRI::102021"); |
| /* |
| * Test creation of CRS having errors. |
| * - Verify error index. |
| */ |
| verifyErroneousCRS(factory, "E1", 69); |
| verifyErroneousCRS(factory, "E2", 42); |
| } |
| |
| /** |
| * Asserts that the description is equal to the expected value. |
| * |
| * @param expected the expected description. |
| * @param factory the factory to use for fetching the description. |
| * @param code the code of the object for which to fetch the description. |
| */ |
| private static void assertDescriptionEquals(String expected, WKTDictionary factory, String code) throws FactoryException { |
| assertEquals(expected, factory.getDescriptionText(IdentifiedObject.class, code).orElseThrow().toString()); |
| } |
| |
| /** |
| * Verifies that there is no duplicated nodes in the {@link StoredTree}s. |
| * When a WKT element is repeated often (e.g. "AngleUnit["Degree", 0.0174532925199433]]"), |
| * only one {@link org.apache.sis.io.wkt.StoredTree.Node} instance should be created and shared by all trees. |
| */ |
| @SuppressWarnings("overloads") // Ambiguous `andThen(…)` method is not used by this test. |
| private static final class SharedValuesCheck implements Consumer<Object>, BiFunction<Integer,Integer,Integer> { |
| /** |
| * Counter of number of occurrences of each instance. Keys may be {@link String}, |
| * {@link Long}, {@link Double} or {@code StoredTree.Node} instances among others. |
| * Values are number of occurrences. |
| */ |
| private final Map<Object,Integer> counts = new IdentityHashMap<>(90); |
| |
| /** |
| * Verifies all trees in the given factory. |
| */ |
| final void verify(final WKTDictionary factory) { |
| factory.forEachValue(this); |
| assertEquals(new HashSet<>(counts.keySet()).size(), counts.size(), |
| "Some values are equal but distinct instances. A single instance should be shared."); |
| /* |
| * Verify the number of occurrences of a few values. Note that the same string representation of keys |
| * value may appear twice: once because the value was already a `String`, and once because the value |
| * was a `StoredTree.Node` with the same string representation. |
| * |
| * The `expected` values below are empirical values and may need to be updated if the content of |
| * `ExtraCRS.txt` test file is modified. |
| */ |
| for (final Map.Entry<Object,Integer> entry : counts.entrySet()) { |
| final String key = entry.getKey().toString(); |
| final int expected; |
| switch (key) { |
| case "Cartesian": expected = 2; break; |
| case "north": expected = 6; break; |
| case "Degree": expected = 6; break; |
| case "Latitude of natural origin": { |
| /* |
| * There is 2 parameters with that string value, but those two parameters are |
| * distinct instances because they have different parameter values (90° and -90°). |
| */ |
| expected = (entry.getKey() instanceof String) ? 2 : 1; |
| break; |
| } |
| default: continue; |
| } |
| assertEquals(expected, entry.getValue().intValue(), key); |
| } |
| } |
| |
| /** |
| * Invoked for each value in a WKT element. This method counts the number of occurrences of each |
| * distinct instance, separated by identity comparison (not by {@link Object#equals(Object)}). |
| */ |
| @Override public void accept(final Object value) { |
| if (value instanceof StoredTree tree) { |
| tree.forEachValue(this); |
| } |
| counts.merge(value, 1, this); |
| } |
| |
| /** Invoked for incrementing a value in the {@link #counts} map. */ |
| @Override public Integer apply(final Integer oldValue, final Integer value) { |
| return oldValue + value; |
| } |
| } |
| |
| /** |
| * Verifies a projected CRS. |
| * |
| * @param crs the CRS to verify. |
| * @param name expected CRS name. |
| * @param φ0 expected latitude of origin. |
| */ |
| private static void verifyCRS(final ProjectedCRS crs, final String name, final double φ0) { |
| assertEquals(name, crs.getName().getCode(), "name"); |
| assertAxisDirectionsEqual(crs.getCoordinateSystem(), |
| new AxisDirection[] {AxisDirection.EAST, AxisDirection.NORTH}, name); |
| assertEquals(φ0, crs.getConversionFromBase().getParameterValues() |
| .parameter("Latitude of natural origin").doubleValue(), "φ0"); |
| } |
| |
| /** |
| * Verifies a geographic CRS. |
| * |
| * @param crs the CRS to verify. |
| * @param name expected CRS name. |
| */ |
| private static void verifyCRS(final GeographicCRS crs, final String name) { |
| assertEquals(name, crs.getName().getCode(), "name"); |
| assertAxisDirectionsEqual(crs.getCoordinateSystem(), |
| new AxisDirection[] {AxisDirection.NORTH, AxisDirection.EAST}, name); |
| } |
| |
| /** |
| * Verifies the error message and error offset when trying to parse an erroneous CRS. |
| * |
| * @param factory factory to use. |
| * @param code code of erroneous CRS. |
| * @param errorOffset expected error index. |
| */ |
| private static void verifyErroneousCRS(final WKTDictionary factory, final String code, final int errorOffset) { |
| FactoryException exception; |
| exception = assertThrows(FactoryException.class, () -> factory.createGeographicCRS(code)); |
| /* |
| * Expect a message like: Cannot create a geodetic object for "E1". |
| * The exact message is locale-dependent, so we cannot test fully. |
| */ |
| assertMessageContains(exception, code); |
| /* |
| * Expect a message like: Missing "semiMajorAxis" component in "Ellipsoid" element. |
| * The error offset (zero-based) should point to the character after "Ellipsoid" in |
| * the following WKT: |
| * |
| * Datum["Erroneous", Ellipsoid["Missing axis length"]] |
| */ |
| final UnparsableObjectException cause = assertInstanceOf(UnparsableObjectException.class, exception.getCause()); |
| assertMessageContains(cause, "Ellipsoid", "semiMajorAxis"); |
| assertEquals(errorOffset, cause.getErrorOffset(), "errorOffset"); |
| /* |
| * Try parsing again. The exception message should have been saved, |
| * i.e. the parsing process is not repeated. |
| */ |
| final String details = cause.getMessage(); |
| exception = assertThrows(FactoryException.class, () -> factory.createGeographicCRS(code)); |
| assertEquals(details, exception.getMessage()); |
| assertNull(exception.getCause()); |
| } |
| |
| /** |
| * Tests {@link WKTDictionary#load(BufferedReader)} with a malformed file. |
| * |
| * @throws IOException if an error occurred while reading the test file. |
| */ |
| @Test |
| public void testLoadMalformed() throws IOException { |
| FactoryException exception; |
| final WKTDictionary factory = new WKTDictionary(null); |
| try (BufferedReader source = new BufferedReader(new InputStreamReader( |
| WKTFormatTest.class.getResourceAsStream("Malformed.txt"), "UTF-8"))) |
| { |
| exception = assertThrows(FactoryException.class, () -> factory.load(source)); |
| } |
| /* |
| * Except a message like: Cannot read file at line 13. Cause is: missing ']' in "GeodCRS" element. |
| * The exact message is locale-dependent, so we test for a few keywords only. |
| */ |
| assertMessageContains(exception, "GeodCRS", "‘]’"); |
| } |
| |
| /** |
| * Tests {@link WKTDictionary#fetchDefinition(DefaultIdentifier)}. |
| * |
| * @throws FactoryException if an error occurred while parsing a WKT. |
| */ |
| @Test |
| public void testFetchDefinition() throws FactoryException { |
| final WKTDictionary factory = new WKTDictionary(null) { |
| @Override protected String fetchDefinition(final DefaultIdentifier identifier) { |
| identifier.setCodeSpace("aNS"); |
| switch (identifier.getCode()) { |
| case "2C": return "GeodCRS[\"Anguilla 1957\",\n" + |
| " Datum[\"Anguilla 1957\",\n" + |
| " Ellipsoid[\"Clarke 1880\", 6378249.145, 293.465]],\n" + |
| " CS[ellipsoidal, 2],\n" + |
| " Axis[\"Latitude\", north],\n" + |
| " Axis[\"Longitude\", east],\n" + |
| " Unit[\"Degree\", 0.0174532925199433],\n" + |
| " Id[\"TEST\", 21]]"; // Intentionally mismatched code. |
| |
| case "2N": return "GeodCRS[\"Anguilla 1957\",\n" + |
| " Datum[\"Anguilla 1957\",\n" + |
| " Ellipsoid[\"Clarke 1880\", 6378249.145, 293.465]],\n" + |
| " CS[ellipsoidal, 2],\n" + |
| " Axis[\"Latitude\", north],\n" + |
| " Axis[\"Longitude\", east],\n" + |
| " Unit[\"Degree\", 0.0174532925199433]]"; |
| |
| default: return null; |
| } |
| } |
| }; |
| /* |
| * Test a CRS with an identifier specified in the WKT. We intentionally declare an ID[…] element |
| * with a different code than the one recognized by the `switch` statement ("2C" versus "21") |
| * for checking precedence. |
| */ |
| GeographicCRS crs = factory.createGeographicCRS("2C"); |
| Identifier id = TestUtilities.getSingleton(crs.getIdentifiers()); |
| assertEquals("TEST", id.getCodeSpace()); |
| assertEquals("21", id.getCode()); |
| assertSame(crs, factory.createGeographicCRS("2C")); // Test caching. |
| /* |
| * Test a CRS without identifier in the WKT. An identifier should be automatically generated |
| * by `WKTFormat.Parser.complete(…)`. |
| */ |
| crs = factory.createGeographicCRS("2N"); |
| id = TestUtilities.getSingleton(crs.getIdentifiers()); |
| assertEquals("aNS", id.getCodeSpace()); |
| assertEquals("2N", id.getCode()); |
| assertSame(crs, factory.createGeographicCRS("2N")); // Test caching. |
| /* |
| * Test non-existent code. |
| */ |
| var exception = assertThrows(FactoryException.class, () -> factory.createGeographicCRS("21")); |
| assertMessageContains(exception, "21"); |
| } |
| } |