/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *    (C) 2005, Institut de Recherche pour le Développement
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.referencing.factory;

// J2SE dependencies
import java.util.Map;
import java.util.logging.Level;

// JUnit dependencies
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

// OpenGIS dependencies
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.Matrix;

// Geotools dependencies
import org.geotools.factory.Hints;
import org.geotools.resources.Arguments;
import org.geotools.referencing.CRS;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.referencing.factory.epsg.LongitudeFirstFactory;


/**
 * Tests the usage of {@link OrderedAxisAuthorityFactory} with the help of the
 * EPSG database. Any EPSG plugin should fit. However, this test live in the
 * {@code plugin/epsg-hsql} module since the HSQL plugin is the only one which
 * is garantee to work on any machine running Maven.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/plugin/epsg-hsql/src/test/java/org/geotools/referencing/factory/OrderedAxisAuthorityFactoryTest.java $
 * @version $Id: OrderedAxisAuthorityFactoryTest.java 27848 2007-11-12 13:10:32Z desruisseaux $
 * @author Martin Desruisseaux
 * @author Jody Garnett
 */
public class OrderedAxisAuthorityFactoryTest extends TestCase {
    /**
     * {@code true} if metadata (especially identifiers) should be erased, or {@code false} if
     * they should be kepts. The {@code true} value matches the pre GEOT-854 state, while the
     * {@code false} value mathes the post GEOT-854 state.
     *
     * @see http://jira.codehaus.org/browse/GEOT-854
     */
    private static final boolean METADATA_ERASED = false;

    /**
     * Small number for floating points comparaisons.
     */
    private static final double EPS = 1E-8;

    /**
     * Run the suite from the command line. If {@code "-log"} flag is specified on the
     * command-line, then the logger will be set to {@link Level#CONFIG}. This is usefull
     * for tracking down which data source is actually used.
     */
    public static void main(final String[] args) {
        final Arguments arguments = new Arguments(args);
        final boolean log = arguments.getFlag("-log");
        arguments.getRemainingArguments(0);
        org.geotools.util.logging.Logging.GEOTOOLS.forceMonolineConsoleOutput(log ? Level.CONFIG : null);
        junit.textui.TestRunner.run(suite());
    }

    /**
     * Returns the test suite.
     */
    public static Test suite() {
        return new TestSuite(OrderedAxisAuthorityFactoryTest.class);
    }

    /**
     * Constructs a test case with the given name.
     */
    public OrderedAxisAuthorityFactoryTest(final String name) {
        super(name);
    }

    /**
     * Returns the ordered axis factory for the specified set of hints.
     */
    private static OrderedAxisAuthorityFactory getFactory(final Hints hints) {
        CRSAuthorityFactory factory;
        factory = ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints);

        assertTrue(factory.getClass().toString(), factory instanceof LongitudeFirstFactory);
        final LongitudeFirstFactory asLongitudeFirst = (LongitudeFirstFactory) factory;
        final Map implementationHints = asLongitudeFirst.getImplementationHints();
        factory = (CRSAuthorityFactory) implementationHints.get(Hints.CRS_AUTHORITY_FACTORY);

        assertTrue(factory.getClass().toString(), factory instanceof OrderedAxisAuthorityFactory);
        final OrderedAxisAuthorityFactory asOrdered = (OrderedAxisAuthorityFactory) factory;
        assertFalse(asOrdered.isCodeMethodOverriden());

        return asOrdered;
    }

    /**
     * Returns a positive number if the specified coordinate system is right-handed,
     * or a negative number if it is left handed.
     */
    private static double getAngle(final CoordinateReferenceSystem crs) {
        final CoordinateSystem cs = crs.getCoordinateSystem();
        assertEquals(2, cs.getDimension());
        return DefaultCoordinateSystemAxis.getAngle(cs.getAxis(0).getDirection(),
                                                    cs.getAxis(1).getDirection());
    }

    /**
     * Tests the registration of the various flavor of {@link OrderedAxisAuthorityFactoryTest}
     * for the EPSG authority factory.
     */
    public void testRegistration() {
        final Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
        OrderedAxisAuthorityFactory factory;
        factory = getFactory(hints);
        assertFalse(factory.forceStandardDirections);
        assertFalse(factory.forceStandardUnits);

        hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.FALSE);
        assertSame(factory, getFactory(hints));
        assertFalse(factory.forceStandardDirections);
        assertFalse(factory.forceStandardUnits);

        hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE);
        assertSame(factory, getFactory(hints));
        assertFalse(factory.forceStandardDirections);
        assertFalse(factory.forceStandardUnits);

        hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.TRUE);
        assertNotSame(factory, factory = getFactory(hints));
        assertFalse  (factory.forceStandardDirections);
        assertTrue   (factory.forceStandardUnits);

        hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.TRUE);
        assertNotSame(factory, factory = getFactory(hints));
        assertTrue   (factory.forceStandardDirections);
        assertTrue   (factory.forceStandardUnits);

        hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE);
        assertNotSame(factory, factory = getFactory(hints));
        assertTrue   (factory.forceStandardDirections);
        assertFalse  (factory.forceStandardUnits);
    }

    /**
     * Tests the axis reordering.
     */
    public void testAxisReordering() throws FactoryException {
        /*
         * Tests the OrderedAxisAuthorityFactory creating using FactoryFinder. The following
         * conditions are not tested directly, but are required in order to get the test to
         * succeed:
         *
         *    - EPSG factories must be provided for both "official" and "modified" axis order.
         *    - The "official" axis order must have precedence over the modified one.
         *    - The hints are correctly understood by FactoryFinder.
         */
        final AbstractAuthorityFactory factory0, factory1;
        final Hints hints = new Hints(Hints.CRS_AUTHORITY_FACTORY, AbstractAuthorityFactory.class);
        factory0 = (AbstractAuthorityFactory) ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints);
        assertFalse(factory0 instanceof OrderedAxisAuthorityFactory);
        assertFalse(factory0 instanceof LongitudeFirstFactory);
        hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
        hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS,   Boolean.TRUE);
        hints.put(Hints.FORCE_STANDARD_AXIS_UNITS,        Boolean.TRUE);
        factory1 = (AbstractAuthorityFactory) ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints);
        assertTrue(factory1 instanceof LongitudeFirstFactory);
        /*
         * The local variables to be used for all remaining tests
         * (usefull to setup in the debugger).
         */
        String code;
        CoordinateReferenceSystem crs0, crs1;
        CoordinateOperationFactory opFactory = ReferencingFactoryFinder.getCoordinateOperationFactory(null);
        MathTransform mt;
        Matrix matrix;
        /*
         * Tests a WGS84 geographic CRS (2D) with (NORTH, EAST) axis directions.
         * The factory should reorder the axis with no more operation than an axis swap.
         */
        code = "4326";
        crs0 = factory0.createCoordinateReferenceSystem(code);
        crs1 = factory1.createCoordinateReferenceSystem(code);
        final CoordinateReferenceSystem cacheTest = crs1;
        assertNotSame(crs0, crs1);
        assertNotSame(crs0.getCoordinateSystem(), crs1.getCoordinateSystem());
        assertSame(((SingleCRS) crs0).getDatum(), ((SingleCRS) crs1).getDatum());
        assertEquals("Expected a left-handed CS.",  -90, getAngle(crs0), EPS);
        assertEquals("Expected a right-handed CS.", +90, getAngle(crs1), EPS);
        assertFalse(crs0.getIdentifiers().isEmpty());
        if (METADATA_ERASED) {
            assertTrue(crs1.getIdentifiers().isEmpty());
        } else {
            assertEquals(crs0.getIdentifiers(), crs1.getIdentifiers());
        }
        mt = opFactory.createOperation(crs0, crs1).getMathTransform();
        assertFalse(mt.isIdentity());
        assertTrue(mt instanceof LinearTransform);
        matrix = ((LinearTransform) mt).getMatrix();
        assertEquals(new GeneralMatrix(new double[][] {
            {0, 1, 0},
            {1, 0, 0},
            {0, 0, 1}}), new GeneralMatrix(matrix));
        /*
         * Tests a WGS84 geographic CRS (3D) with (NORTH, EAST, UP) axis directions.
         * Because this CRS uses sexagesimal units, conversions are not supported and
         * will not be tested.
         */
        code = "4329";
        crs0 = factory0.createCoordinateReferenceSystem(code);
        crs1 = factory1.createCoordinateReferenceSystem(code);
        assertNotSame(crs0, crs1);
        assertNotSame(crs0.getCoordinateSystem(), crs1.getCoordinateSystem());
        assertSame(((SingleCRS) crs0).getDatum(), ((SingleCRS) crs1).getDatum());
        assertFalse(crs0.getIdentifiers().isEmpty());
        if (METADATA_ERASED) {
            assertTrue(crs1.getIdentifiers().isEmpty());
        } else {
            assertEquals(crs0.getIdentifiers(), crs1.getIdentifiers());
        }
        /*
         * Tests a WGS84 geographic CRS (3D) with (NORTH, EAST, UP) axis directions.
         * The factory should reorder the axis with no more operation than an axis swap.
         */
        code = "63266413";
        crs0 = factory0.createCoordinateReferenceSystem(code);
        crs1 = factory1.createCoordinateReferenceSystem(code);
        assertNotSame(crs0, crs1);
        assertNotSame(crs0.getCoordinateSystem(), crs1.getCoordinateSystem());
        assertSame(((SingleCRS) crs0).getDatum(), ((SingleCRS) crs1).getDatum());
        assertFalse(crs0.getIdentifiers().isEmpty());
        if (METADATA_ERASED) {
            assertTrue(crs1.getIdentifiers().isEmpty());
        } else {
            assertEquals(crs0.getIdentifiers(), crs1.getIdentifiers());
        }
        mt = opFactory.createOperation(crs0, crs1).getMathTransform();
        assertFalse(mt.isIdentity());
        assertTrue(mt instanceof LinearTransform);
        matrix = ((LinearTransform) mt).getMatrix();
        assertEquals(new GeneralMatrix(new double[][] {
            {0, 1, 0, 0},
            {1, 0, 0, 0},
            {0, 0, 1, 0},
            {0, 0, 0, 1}}), new GeneralMatrix(matrix));
        /*
         * Tests a projected CRS with (EAST, NORTH) axis orientation. No axis reordering is needed,
         * which means that their coordinate systems are identical and the math transform should be
         * the identity one. Note that while no axis swap is needed, the base GeographicCRS are not
         * the same since an axis reordering has been done there.
         */
        code = "2027";
        crs0 = factory0.createCoordinateReferenceSystem(code);
        crs1 = factory1.createCoordinateReferenceSystem(code);
        assertNotSame(crs0, crs1);
        assertSame(crs0.getCoordinateSystem(), crs1.getCoordinateSystem());
        assertSame(((SingleCRS) crs0).getDatum(), ((SingleCRS) crs1).getDatum());
        assertNotSame(((ProjectedCRS) crs0).getBaseCRS(), ((ProjectedCRS) crs1).getBaseCRS());
        assertFalse(crs0.getIdentifiers().isEmpty());
        if (METADATA_ERASED) {
            assertTrue(crs1.getIdentifiers().isEmpty());
        } else {
            assertEquals(crs0.getIdentifiers(), crs1.getIdentifiers());
        }
        mt = opFactory.createOperation(crs0, crs1).getMathTransform();
        assertTrue(mt.isIdentity());
        /*
         * Tests a projected CRS with (WEST, SOUTH) axis orientation.
         * The factory should arrange the axis with no more operation than a direction change.
         * While the end result is a matrix like the GeographicCRS case, the path that lead to
         * this result is much more complex.
         */
        code = "22275";
        crs0 = factory0.createCoordinateReferenceSystem(code);
        crs1 = factory1.createCoordinateReferenceSystem(code);
        assertNotSame(crs0, crs1);
        assertNotSame(crs0.getCoordinateSystem(), crs1.getCoordinateSystem());
        assertSame(((SingleCRS) crs0).getDatum(), ((SingleCRS) crs1).getDatum());
        assertFalse(crs0.getIdentifiers().isEmpty());
        if (METADATA_ERASED) {
            assertTrue(crs1.getIdentifiers().isEmpty());
        } else {
            assertEquals(crs0.getIdentifiers(), crs1.getIdentifiers());
        }
        mt = opFactory.createOperation(crs0, crs1).getMathTransform();
        assertFalse(mt.isIdentity());
        assertTrue(mt instanceof LinearTransform);
        matrix = ((LinearTransform) mt).getMatrix();
        assertEquals(new GeneralMatrix(new double[][] {
            {-1,  0,  0},
            { 0, -1,  0},
            { 0,  0,  1}}), new GeneralMatrix(matrix));
        /*
         * Tests the cache.
         */
        assertSame(cacheTest, factory1.createCoordinateReferenceSystem("4326"));
    }

    /**
     * Tests the creation of EPSG:4326 CRS with different axis order.
     */
    public void testLongitudeFirst() throws FactoryException {
        final CoordinateReferenceSystem standard = CRS.decode("EPSG:4326", false);
        final CoordinateReferenceSystem modified = CRS.decode("EPSG:4326", true );
        assertEquals("Expected a left-handed CS.",  -90, getAngle(standard), EPS);
        assertEquals("Expected a right-handed CS.", +90, getAngle(modified), EPS);
        final MathTransform transform = CRS.findMathTransform(standard, modified);
        assertTrue(transform instanceof LinearTransform);
        final Matrix matrix = ((LinearTransform) transform).getMatrix();
        assertEquals(new GeneralMatrix(new double[][] {
            { 0,  1,  0},
            { 1,  0,  0},
            { 0,  0,  1}}), new GeneralMatrix(matrix));
    }

    /**
     * Tests the {@link IdentifiedObjectFinder#find} method with axis order forced.
     */
    public void testFind() throws FactoryException {
        final CRSAuthorityFactory factory = ReferencingFactoryFinder.getCRSAuthorityFactory(
                "EPSG", new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE));
        assertTrue(factory instanceof AbstractAuthorityFactory);
        final IdentifiedObjectFinder finder = ((AbstractAuthorityFactory) factory).
                getIdentifiedObjectFinder(CoordinateReferenceSystem.class);
        /*
         * We tested in DefaultFactoryTest that WGS84 is not found when searching
         * directly in DefaultFactory. Now we perform the same search through the
         * ordered axis authority factory.
         */
        finder.setFullScanAllowed(false);
        assertNull("Should not find the CRS without a scan.",
                   finder.find(DefaultGeographicCRS.WGS84));

        finder.setFullScanAllowed(true);
        IdentifiedObject find = finder.find(DefaultGeographicCRS.WGS84);
        assertNotNull("With scan allowed, should find the CRS.", find);
        assertTrue(CRS.equalsIgnoreMetadata(DefaultGeographicCRS.WGS84, find));
        assertEquals("Expected a right-handed CS.", +90, getAngle((CoordinateReferenceSystem) find), EPS);
        /*
         * Search a CRS using (latitude,longitude) axis order. The IdentifiedObjectFinder
         * should be able to find it even if it is backed by LongitudeFirstAuthorityFactory,
         * because the later is itself backed by EPSG factory and IdentifiedObjectFinder
         * should queries CRS from both.
         */
        final String wkt =
                "GEOGCS[\"WGS 84\",\n" +
                "  DATUM[\"WGS84\",\n" +
                "    SPHEROID[\"WGS 84\", 6378137.0, 298.257223563]],\n" +
                "  PRIMEM[\"Greenwich\", 0.0],\n" +
                "  UNIT[\"degree\", 0.017453292519943295],\n" +
                "  AXIS[\"Geodetic latitude\", NORTH],\n" + 
                "  AXIS[\"Geodetic longitude\", EAST]]";
        final CoordinateReferenceSystem search   = CRS.parseWKT(wkt);
        final CoordinateReferenceSystem standard = CRS.decode("EPSG:4326", false);
        assertTrue(CRS.equalsIgnoreMetadata(search, standard));
        assertFalse("Identifiers should not be the same.", search.equals(standard));
        finder.setFullScanAllowed(false);
        assertNull("Should not find the CRS without a scan.", finder.find(search));

        finder.setFullScanAllowed(true);
        find = finder.find(search);
        final CoordinateReferenceSystem crs = (CoordinateReferenceSystem) search;
        assertNotNull("Should find the CRS despite the different axis order.", find);
        assertEquals("Expected a left-handed CS.", -90, getAngle(crs), EPS);
        assertFalse(CRS.equalsIgnoreMetadata(find, DefaultGeographicCRS.WGS84));
        assertTrue (CRS.equalsIgnoreMetadata(find, search));
        assertTrue (CRS.equalsIgnoreMetadata(find, standard));
        assertSame("Expected caching to work.", standard, find);
    }
}
