/*
 * 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.internal.referencing.provider;

import java.net.URISyntaxException;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import org.opengis.geometry.Envelope;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.TestStep;
import org.junit.Test;

import static org.opengis.test.Assert.*;


/**
 * Tests {@link FranceGeocentricInterpolation}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.7
 * @since 0.7
 * @module
 */
public final strictfp class FranceGeocentricInterpolationTest extends DatumShiftTestCase {
    /**
     * Name of the file containing a small extract of the "{@code GR3DF97A.txt}" file.
     * The amount of data in this test file is less than 0.14% of the original file.
     */
    public static final String TEST_FILE = "GR3DF-extract.txt";

    /**
     * Returns the sample point for a step in the example given by the NTG_88 guidance note.
     * The steps numbers go from 1 (NTF) to 3 (RGF93).
     * Precision is given by {@link #ANGULAR_TOLERANCE}.
     *
     * <blockquote><b>Source:</b>
     * <cite>"Grille de paramètres de transformation de coordonnées GR3DF97A"</cite>
     * version 1.0, April 1997 in <a href="http://www.ign.fr">http://www.ign.fr</a>
     * </blockquote>
     *
     * @param  step  the step as a value from 1 to 3 inclusive.
     * @return the sample point at the given step.
     */
    public static double[] samplePoint(final int step) {
        switch (step) {
            case 1: return new double[] {           // NTF
                         2 + (25 + 32.4187/60)/60,  //  2°25′32.4187″
                        48 + (50 + 40.2441/60)/60   // 48°50′40.2441″
                    };
            case 2: return new double[] {           // RGF93 with constant (ΔX,ΔY,ΔZ)
                         2 + (25 + 29.8273/60)/60,  //  2°25′29.8273″
                        48 + (50 + 39.9967/60)/60   // 48°50′39.9967″
                    };
            case 3: return new double[] {           // RGF93 with interpolated (ΔX,ΔY,ΔZ)
                         2 + (25 + 29.89599/60)/60, //  2°25′29.89599″
                        48 + (50 + 40.00502/60)/60  // 48°50′40.00502″
                    };
            default: throw new AssertionError(step);
        }
    }

    /**
     * The precision of values returned by {@link #samplePoint(int)}, in degrees.
     * Use as the tolerance threshold in assertions.
     */
    public static final double ANGULAR_TOLERANCE = (0.0001 / 60 / 60) / 2;

    /**
     * Tests {@link FranceGeocentricInterpolation#isRecognized(Path)}.
     */
    @Test
    public void testIsRecognized() {
        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("GR3DF97A.txt")));
        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("gr3df")));
        assertFalse(FranceGeocentricInterpolation.isRecognized(Paths.get("gr3d")));
        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get(TEST_FILE)));
    }

    /**
     * Tests {@link FranceGeocentricInterpolation#redimension(int, int)}.
     */
    @Test
    public void testRedimension() {
        MolodenskyTest.testRedimension(new FranceGeocentricInterpolation());
    }

    /**
     * Tests a small grid file with interpolations in geocentric coordinates.
     *
     * @throws URISyntaxException if the URL to the test file can not be converted to a path.
     * @throws IOException if an error occurred while loading the grid.
     * @throws FactoryException if an error occurred while computing the grid.
     * @throws TransformException if an error occurred while computing the envelope.
     */
    @Test
    public void testGrid() throws URISyntaxException, IOException, FactoryException, TransformException {
        testGridAsShorts(testGridAsFloats());
    }

    /**
     * Tests a small grid file with interpolations in geocentric coordinates as {@code float} values.
     *
     * <p>This method is part of a chain.
     * The next method is {@link #testGridAsShorts(DatumShiftGridFile)}.</p>
     *
     * @return the loaded grid with values as {@code float}.
     * @throws URISyntaxException if the URL to the test file can not be converted to a path.
     * @throws IOException if an error occurred while loading the grid.
     * @throws FactoryException if an error occurred while computing the grid.
     * @throws TransformException if an error occurred while computing the envelope.
     */
    @TestStep
    private static DatumShiftGridFile<Angle,Length> testGridAsFloats()
            throws URISyntaxException, IOException, FactoryException, TransformException
    {
        final Path file = getResource(TEST_FILE);
        final DatumShiftGridFile.Float<Angle,Length> grid;
        try (BufferedReader in = Files.newBufferedReader(file)) {
            grid = FranceGeocentricInterpolation.load(in, file);
        }
        assertEquals("cellPrecision",   0.005, grid.getCellPrecision(), STRICT);
        assertEquals("getCellMean",  168.2587, grid.getCellMean(0), 0.0001);
        assertEquals("getCellMean",   58.7163, grid.getCellMean(1), 0.0001);
        assertEquals("getCellMean", -320.1801, grid.getCellMean(2), 0.0001);
        verifyGrid(grid);
        return grid;
    }

    /**
     * Tests a small grid file with interpolations in geocentric coordinates as {@code short} values.
     *
     * <p>This method is part of a chain.
     * The previous method is {@link #testGridAsFloats()}.</p>
     *
     * @param  grid  the grid created by {@link #testGridAsFloats()}.
     * @return the given grid, but compressed as {@code short} values.
     * @throws TransformException if an error occurred while computing the envelope.
     */
    @TestStep
    private static DatumShiftGridFile<Angle,Length> testGridAsShorts(DatumShiftGridFile<Angle,Length> grid)
            throws TransformException
    {
        grid = DatumShiftGridCompressed.compress((DatumShiftGridFile.Float<Angle,Length>) grid, new double[] {
                FranceGeocentricInterpolation.TX,           //  168 metres
                FranceGeocentricInterpolation.TY,           //   60 metres
                FranceGeocentricInterpolation.TZ},          // -320 metres
                FranceGeocentricInterpolation.PRECISION);
        assertInstanceOf("Failed to compress 'float' values into 'short' values.", DatumShiftGridCompressed.class, grid);
        assertEquals("cellPrecision", 0.0005, grid.getCellPrecision(), STRICT);
        assertEquals("getCellMean",  168, grid.getCellMean(0), STRICT);
        assertEquals("getCellMean",   60, grid.getCellMean(1), STRICT);
        assertEquals("getCellMean", -320, grid.getCellMean(2), STRICT);
        verifyGrid(grid);
        return grid;
    }

    /**
     * Verifies the envelope and the interpolation performed by the given grid.
     *
     * @throws TransformException if an error occurred while computing the envelope.
     */
    private static void verifyGrid(final DatumShiftGridFile<Angle,Length> grid) throws TransformException {
        final Envelope envelope = grid.getDomainOfValidity();
        assertEquals("xmin",  2.2 - 0.05, envelope.getMinimum(0), 1E-12);
        assertEquals("xmax",  2.5 + 0.05, envelope.getMaximum(0), 1E-12);
        assertEquals("ymin", 48.5 - 0.05, envelope.getMinimum(1), 1E-12);
        assertEquals("ymax", 49.0 + 0.05, envelope.getMaximum(1), 1E-12);
        /*
         * The values in the NTG_88 document are:
         *
         * (gridX=2, gridY=3)  00002    2.400000000   48.800000000  -168.252   -58.630   320.170  01   2314
         * (gridX=2, gridY=4)  00002    2.400000000   48.900000000  -168.275   -58.606   320.189  01   2314
         * (gridX=3, gridY=3)  00002    2.500000000   48.800000000  -168.204   -58.594   320.125  01   2314
         * (gridX=3, gridY=4)  00002    2.500000000   48.900000000  -168.253   -58.554   320.165  01   2314
         *
         * Directions (signs) are reversed compared to NTG_88 document.
         */
        assertEquals("translationDimensions", 3, grid.getTranslationDimensions());
        assertEquals("grid.accuracy",      0.05, grid.accuracy,              STRICT);
        assertEquals("getCellValue",    168.196, grid.getCellValue(0, 2, 1), STRICT);
        assertEquals("getCellValue",     58.778, grid.getCellValue(1, 2, 1), STRICT);
        assertEquals("getCellValue",   -320.127, grid.getCellValue(2, 2, 1), STRICT);
        /*
         * Interpolate the (ΔX, ΔY, ΔZ) at a point.
         * Directions (signs) are reversed compared to NTG_88 document.
         */
        final double[] expected = {
             168.253,       // ΔX: Toward prime meridian
              58.609,       // ΔY: Toward 90° east
            -320.170        // ΔZ: Toward north pole
        };
        final double[] point  = samplePoint(3);
        final double[] vector = grid.interpolateAt(point[0], point[1]);
        assertArrayEquals("(ΔX, ΔY, ΔZ)", expected, vector, 0.0005);
    }

    /**
     * Tests the {@link FranceGeocentricInterpolation#getOrLoad(Path, double[], double)} method and its cache.
     *
     * @throws URISyntaxException if the URL to the test file can not be converted to a path.
     * @throws FactoryException if an error occurred while computing the grid.
     * @throws TransformException if an error occurred while computing the envelope.
     */
    @Test
    @DependsOnMethod("testGrid")
    public void testGetOrLoad() throws URISyntaxException, FactoryException, TransformException {
        final DatumShiftGridFile<Angle,Length> grid = FranceGeocentricInterpolation.getOrLoad(
                getResource(TEST_FILE), new double[] {
                        FranceGeocentricInterpolation.TX,
                        FranceGeocentricInterpolation.TY,
                        FranceGeocentricInterpolation.TZ},
                        FranceGeocentricInterpolation.PRECISION);
        verifyGrid(grid);
        assertSame("Expected a cached value.", grid, FranceGeocentricInterpolation.getOrLoad(
                getResource(TEST_FILE), new double[] {
                        FranceGeocentricInterpolation.TX,
                        FranceGeocentricInterpolation.TY,
                        FranceGeocentricInterpolation.TZ},
                        FranceGeocentricInterpolation.PRECISION));
    }
}
