/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.referencing.operation.projection;

import java.util.Random;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea;
import org.apache.sis.referencing.operation.DefaultOperationMethod;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.DependsOn;
import org.junit.Test;

import static java.lang.StrictMath.*;
import static org.apache.sis.math.MathFunctions.atanh;
import static org.junit.Assert.assertEquals;


/**
 * Tests {@link EqualAreaProjection}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   1.0
 * @module
 */
@DependsOn(NormalizedProjectionTest.class)
public final strictfp class EqualAreaProjectionTest extends MapProjectionTestCase {
    /**
     * Creates the projection to be tested.
     *
     * @param  ellipsoidal   {@code false} for a sphere, or {@code true} for WGS84 ellipsoid.
     * @return a test instance of the projection.
     */
    private EqualAreaProjection create(final boolean ellipsoidal) {
        final DefaultOperationMethod provider = new LambertCylindricalEqualArea();
        final CylindricalEqualArea projection = new CylindricalEqualArea(provider, parameters(provider, ellipsoidal));
        tolerance = NormalizedProjection.ANGULAR_TOLERANCE;     // = linear tolerance on a sphere of radius 1.
        return projection;
    }

    /**
     * Computes φ using equation given in EPSG guidance notes, which is also from Snyder book.
     * We use this equation as a reference for testing validity of other forms.
     *
     * @param  y  in the cylindrical case, this is northing on the normalized ellipsoid.
     * @return the latitude in radians.
     */
    private static double reference(final EqualAreaProjection projection, final double y) {
        final double e    = projection.eccentricity;
        final double e2   = projection.eccentricitySquared;
        final double e4   = e2 * e2;
        final double e6   = e2 * e4;
        final double c2β  = 517./5040  * e6  +  31./180 * e4  +  1./3 * e2;
        final double c4β  = 251./3780  * e6  +  23./360 * e4;
        final double c6β  = 761./45360 * e6;
        final double qmp  = (1/(1 - e*e) + atanh(e)/e);
        final double sinβ = y / qmp;
        final double β    = asin(sinβ);
        return c6β * sin(6*β)
             + c4β * sin(4*β)
             + c2β * sin(2*β)
             + β;                           // Snyder 3-18
    }

    /**
     * Compares {@link EqualAreaProjection#φ(double)} with formula taken as references.
     *
     * @throws ProjectionException if the function does not converge.
     */
    @Test
    public void compareWithReference() throws ProjectionException {
        final EqualAreaProjection projection = create(true);
        final Random random = TestUtilities.createRandomNumberGenerator();
        for (int i=0; i<100; i++) {
            final double y = random.nextDouble() * 3 - 1.5;
            final double reference = reference(projection, y);
            final double actual    = projection.φ(y);
            assertEquals(reference, actual, NormalizedProjection.ITERATION_TOLERANCE);
        }
    }

    /**
     * Searches a value for {@link EqualAreaProjection#ECCENTRICITY_THRESHOLD}.
     * This method is not part of test suite. Steps to enable:
     *
     * <ol>
     *   <li>In {@link EqualAreaProjection#φ(double)} method, for {@code useIterations} to {@code false}.</li>
     *   <li>Add a {@link Test} annotation on this method.
     * </ol>
     *
     * @throws ProjectionException if {@link EqualAreaProjection#φ(double)} did not converge.
     */
    @SuppressWarnings("UseOfSystemOutOrSystemErr")
    public void searchThreshold() throws ProjectionException {
        tolerance = NormalizedProjection.ANGULAR_TOLERANCE;
        final DefaultOperationMethod provider = new LambertCylindricalEqualArea();
        final Parameters parameters = parameters(provider, true);
        for (double e = 0.05; e <= 0.2; e += 0.001) {
            final double a = parameters.parameter(Constants.SEMI_MAJOR).doubleValue();
            parameters.parameter(Constants.SEMI_MINOR).setValue(a * sqrt(1 - e*e));
            final CylindricalEqualArea projection = new CylindricalEqualArea(provider, parameters);
            for (double y = -1.25; y <= 1.25; y += 0.01) {
                final double reference = reference(projection, y);
                final double actual = projection.φ(y);
                if (abs(actual - reference) > NormalizedProjection.ANGULAR_TOLERANCE) {
                    System.out.println("Error exceeds tolerance threshold at eccentricity " + e);
                    return;
                }
            }
        }
    }
}
