WMMLoader.java

/*
 * Copyright (C) 2020 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed 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 com.irurueta.navigation.inertial.wmm;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * Loads a WWM from a file of coefficients.
 * The file of coefficients is updated every 5 years and can be obtained
 * at: <a href="https://www.ngdc.noaa.gov/geomag/WMM/">https://www.ngdc.noaa.gov/geomag/WMM/</a>
 *
 * @see WorldMagneticModel
 */
public class WMMLoader {

    /**
     * Maximum allowed value within file of coefficients.
     */
    private static final double MAX_VAL = 9999.0;

    /**
     * Number of coefficients.
     */
    private static final int N = WorldMagneticModel.N;

    /**
     * Constructor.
     * Prevents instantiation of helper class.
     */
    private WMMLoader() {
    }

    /**
     * Loads World Magnetic Model from provided resource name.
     * Resource will be resolved an loaded using current class loader.
     *
     * @param resource a resource name.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel loadFromResource(final String resource) throws IOException {
        try (final var stream = WMMLoader.class.getResourceAsStream(resource)) {
            return load(stream);
        }
    }

    /**
     * Loads World Magnetic Model from provided URL.
     * Data will be requested with a "GET" method without additional headers.
     *
     * @param url URL to request data from.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel loadFromUrl(final String url) throws IOException {
        return load(new URL(url));
    }

    /**
     * Loads World Magnetic Model from provided file path.
     *
     * @param filePath a file path.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel loadFromFile(final String filePath) throws IOException {
        try (final var stream = new FileInputStream(filePath)) {
            return load(stream);
        }
    }

    /**
     * Loads World Magnetic Model from provided URL.
     * Data will be requested with a "GET" method without additional headers.
     *
     * @param url URL to request data from.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel load(final URL url) throws IOException {
        final var connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        try (final var stream = connection.getInputStream()) {
            return load(stream);
        }
    }

    /**
     * Loads World Magnetic Model from provided file.
     *
     * @param file a file.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel load(final File file) throws IOException {
        try (final var stream = new FileInputStream(file)) {
            return load(stream);
        }
    }

    /**
     * Loads World Magnetic Model from provided stream of data.
     *
     * @param stream a stream of data.
     * @return a World Magnetic Model containing all required coefficients.
     * @throws IOException if an I/O error occurs.
     */
    public static WorldMagneticModel load(final InputStream stream) throws IOException {
        try (final var reader = new InputStreamReader(stream)) {
            final var result = new WorldMagneticModel();
            final var tokenizer = new StreamTokenizer(reader);

            // Read World Magnetic Model spherical harmonic coefficients
            result.snorm[0] = 1.0;
            result.c[0][0] = 0.0;
            result.cd[0][0] = 0.0;

            tokenizer.nextToken();
            result.epoch = tokenizer.nval;
            tokenizer.nextToken();
            tokenizer.nextToken();

            // loop to get data from file
            while (true) {
                tokenizer.nextToken();
                if (tokenizer.nval >= MAX_VAL) {
                    // end of file
                    break;
                }

                final var n = (int) tokenizer.nval;
                tokenizer.nextToken();
                final var m = (int) tokenizer.nval;
                tokenizer.nextToken();
                final var gnm = tokenizer.nval;
                tokenizer.nextToken();
                final var hnm = tokenizer.nval;
                tokenizer.nextToken();
                final var dgnm = tokenizer.nval;
                tokenizer.nextToken();
                final var dhnm = tokenizer.nval;

                if (m <= n) {
                    result.c[m][n] = gnm;
                    result.cd[m][n] = dgnm;

                    if (m != 0) {
                        result.c[n][m - 1] = hnm;
                        result.cd[n][m - 1] = dhnm;
                    }
                }
            }

            // convert Schmidt normalized Gauss coefficients to un-normalized
            result.snorm[0] = 1.0;
            for (var n = 1; n <= WorldMagneticModel.MAX_ORDER; n++) {

                result.snorm[n] = result.snorm[n - 1] * (2 * n - 1) / n;
                var j = 2;

                for (int m = 0, D1 = 1, D2 = (n - m + D1) / D1; D2 > 0; D2--, m += D1) {
                    result.k[m][n] = (double) (((n - 1) * (n - 1)) - (m * m)) / (double) ((2 * n - 1) * (2 * n - 3));
                    if (m > 0) {
                        final var flnmj = ((n - m + 1) * j) / (double) (n + m);
                        result.snorm[n + m * N] = result.snorm[n + (m - 1) * N] * Math.sqrt(flnmj);
                        j = 1;
                        result.c[n][m - 1] = result.snorm[n + m * N] * result.c[n][m - 1];
                        result.cd[n][m - 1] = result.snorm[n + m * N] * result.cd[n][m - 1];
                    }
                    result.c[m][n] = result.snorm[n + m * N] * result.c[m][n];
                    result.cd[m][n] = result.snorm[n + m * N] * result.cd[m][n];
                }

                result.fn[n] = (n + 1);
                result.fm[n] = n;
            }

            result.k[1][1] = 0.0;

            return result;
        }
    }
}