LazyLoadedCelestialBodies.java

/* Contributed in the public domain.
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS 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.orekit.bodies;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.orekit.data.DataProvidersManager;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.frames.Frame;
import org.orekit.time.TimeScales;

/**
 * This class lazily loads auxiliary data when it is needed by a requested body. It is
 * designed to match the behavior of {@link CelestialBodyFactory} in Orekit 10.0.
 *
 * @author Luc Maisonobe
 * @author Evan Ward
 * @see CelestialBodyFactory
 * @since 10.1
 */
public class LazyLoadedCelestialBodies implements CelestialBodies {

    /** Supplies the auxiliary data files. */
    private final DataProvidersManager dataProvidersManager;
    /** Provides access to time scales when parsing bodies. */
    private final TimeScales timeScales;
    /** Earth centered frame aligned with ICRF. */
    private final Frame gcrf;
    /** Celestial body loaders map. */
    private final Map<String, List<CelestialBodyLoader>> loadersMap = new HashMap<>();

    /** Celestial body map. */
    private final Map<String, CelestialBody> celestialBodyMap = new HashMap<>();

    /**
     * Create a celestial body factory with the given auxiliary data sources.
     *
     * @param dataProvidersManager supplies JPL ephemerides auxiliary data files.
     * @param timeScales           set of time scales to use when loading bodies.
     * @param gcrf                 Earth centered frame aligned with ICRF.
     */
    public LazyLoadedCelestialBodies(final DataProvidersManager dataProvidersManager,
                                     final TimeScales timeScales,
                                     final Frame gcrf) {
        this.dataProvidersManager = dataProvidersManager;
        this.timeScales = timeScales;
        this.gcrf = gcrf;
    }

    /** Add a loader for celestial bodies.
     * @param name name of the body (may be one of the predefined names or a user-defined name)
     * @param loader custom loader to add for the body
     * @see #addDefaultCelestialBodyLoader(String)
     * @see #clearCelestialBodyLoaders(String)
     * @see #clearCelestialBodyLoaders()
     */
    public void addCelestialBodyLoader(final String name,
                                       final CelestialBodyLoader loader) {
        synchronized (loadersMap) {
            loadersMap.computeIfAbsent(name, k -> new ArrayList<>()).add(loader);
        }
    }

    /** Add the default loaders for all predefined celestial bodies.
     * @param supportedNames regular expression for supported files names
     * (may be null if the default JPL file names are used)
     * <p>
     * The default loaders look for DE405 or DE406 JPL ephemerides.
     * </p>
     * @see <a href="ftp://ssd.jpl.nasa.gov/pub/eph/planets/Linux/de405/">DE405 JPL ephemerides</a>
     * @see <a href="ftp://ssd.jpl.nasa.gov/pub/eph/planets/Linux/de406/">DE406 JPL ephemerides</a>
     * @see #addCelestialBodyLoader(String, CelestialBodyLoader)
     * @see #addDefaultCelestialBodyLoader(String)
     * @see #clearCelestialBodyLoaders(String)
     * @see #clearCelestialBodyLoaders()
     */
    public void addDefaultCelestialBodyLoader(final String supportedNames) {
        addDefaultCelestialBodyLoader(CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER, supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.SUN,                     supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.MERCURY,                 supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.VENUS,                   supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.EARTH_MOON,              supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.EARTH,                   supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.MOON,                    supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.MARS,                    supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.JUPITER,                 supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.SATURN,                  supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.URANUS,                  supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.NEPTUNE,                 supportedNames);
        addDefaultCelestialBodyLoader(CelestialBodyFactory.PLUTO,                   supportedNames);
    }

    /** Add the default loaders for celestial bodies.
     * @param name name of the body (if not one of the predefined names, the method does nothing)
     * @param supportedNames regular expression for supported files names
     * (may be null if the default JPL file names are used)
     * <p>
     * The default loaders look for DE405 or DE406 JPL ephemerides.
     * </p>
     * @see <a href="ftp://ssd.jpl.nasa.gov/pub/eph/planets/Linux/de405/">DE405 JPL ephemerides</a>
     * @see <a href="ftp://ssd.jpl.nasa.gov/pub/eph/planets/Linux/de406/">DE406 JPL ephemerides</a>
     * @see #addCelestialBodyLoader(String, CelestialBodyLoader)
     * @see #addDefaultCelestialBodyLoader(String)
     * @see #clearCelestialBodyLoaders(String)
     * @see #clearCelestialBodyLoaders()
     */
    public void addDefaultCelestialBodyLoader(final String name,
                                              final String supportedNames) {

        CelestialBodyLoader loader = null;
        if (name.equalsIgnoreCase(CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.SOLAR_SYSTEM_BARYCENTER, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.SUN)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.SUN, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.MERCURY)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.MERCURY, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.VENUS)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.VENUS, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.EARTH_MOON)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.EARTH_MOON, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.EARTH)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.EARTH, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.MOON)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.MOON, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.MARS)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.MARS, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.JUPITER)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.JUPITER, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.SATURN)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.SATURN, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.URANUS)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.URANUS, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.NEPTUNE)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.NEPTUNE, dataProvidersManager, timeScales, gcrf);
        } else if (name.equalsIgnoreCase(CelestialBodyFactory.PLUTO)) {
            loader =
                    new JPLEphemeridesLoader(supportedNames, JPLEphemeridesLoader.EphemerisType.PLUTO, dataProvidersManager, timeScales, gcrf);
        }

        if (loader != null) {
            addCelestialBodyLoader(name, loader);
        }

    }

    /** Clear loaders for one celestial body.
     * <p>
     * Calling this method also clears the celestial body that
     * has been loaded via this {@link CelestialBodyLoader}.
     * </p>
     * @param name name of the body
     * @see #addCelestialBodyLoader(String, CelestialBodyLoader)
     * @see #clearCelestialBodyLoaders()
     * @see #clearCelestialBodyCache(String)
     */
    public void clearCelestialBodyLoaders(final String name) {
        // use same synchronization order as in getBody to prevent deadlocks
        synchronized (celestialBodyMap) {
            // take advantage of reentrent synchronization as
            // clearCelestialBodyCache uses the same lock inside
            clearCelestialBodyCache(name);

            synchronized (loadersMap) {
                loadersMap.remove(name);
            }
        }
    }

    /** Clear loaders for all celestial bodies.
     * <p>
     * Calling this method also clears all loaded celestial bodies.
     * </p>
     * @see #addCelestialBodyLoader(String, CelestialBodyLoader)
     * @see #clearCelestialBodyLoaders(String)
     * @see #clearCelestialBodyCache()
     */
    public void clearCelestialBodyLoaders() {
        synchronized (celestialBodyMap) {
            clearCelestialBodyCache();

            synchronized (loadersMap) {
                loadersMap.clear();
            }
        }
    }

    /** Clear the specified celestial body from the internal cache.
     * @param name name of the body
     */
    public void clearCelestialBodyCache(final String name) {
        synchronized (celestialBodyMap) {
            celestialBodyMap.remove(name);
        }
    }

    /** Clear all loaded celestial bodies.
     * <p>
     * Calling this method will remove all loaded bodies from the internal
     * cache. Subsequent calls to {@link #getBody(String)} or similar methods
     * will result in a reload of the requested body from the configured loader(s).
     * </p>
     */
    public void clearCelestialBodyCache() {
        synchronized (celestialBodyMap) {
            celestialBodyMap.clear();
        }
    }

    @Override
    public CelestialBody getSolarSystemBarycenter() {
        return getBody(CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER);
    }

    @Override
    public CelestialBody getSun() {
        return getBody(CelestialBodyFactory.SUN);
    }

    @Override
    public CelestialBody getMercury() {
        return getBody(CelestialBodyFactory.MERCURY);
    }

    @Override
    public CelestialBody getVenus() {
        return getBody(CelestialBodyFactory.VENUS);
    }

    @Override
    public CelestialBody getEarthMoonBarycenter() {
        return getBody(CelestialBodyFactory.EARTH_MOON);
    }

    @Override
    public CelestialBody getEarth() {
        return getBody(CelestialBodyFactory.EARTH);
    }

    @Override
    public CelestialBody getMoon() {
        return getBody(CelestialBodyFactory.MOON);
    }

    @Override
    public CelestialBody getMars() {
        return getBody(CelestialBodyFactory.MARS);
    }

    @Override
    public CelestialBody getJupiter() {
        return getBody(CelestialBodyFactory.JUPITER);
    }

    @Override
    public CelestialBody getSaturn() {
        return getBody(CelestialBodyFactory.SATURN);
    }

    @Override
    public CelestialBody getUranus() {
        return getBody(CelestialBodyFactory.URANUS);
    }

    @Override
    public CelestialBody getNeptune() {
        return getBody(CelestialBodyFactory.NEPTUNE);
    }

    @Override
    public CelestialBody getPluto() {
        return getBody(CelestialBodyFactory.PLUTO);
    }

    /**
     * {@inheritDoc}
     *
     * <p>
     * If no {@link CelestialBodyLoader} has been added by calling {@link
     * #addCelestialBodyLoader(String, CelestialBodyLoader) addCelestialBodyLoader} or if
     * {@link #clearCelestialBodyLoaders(String) clearCelestialBodyLoaders} has been
     * called afterwards, the {@link #addDefaultCelestialBodyLoader(String, String)
     * addDefaultCelestialBodyLoader} method will be called automatically, once with the
     * default name for JPL DE ephemerides and once with the default name for IMCCE INPOP
     * files.
     * </p>
     */
    @Override
    public CelestialBody getBody(final String name) {
        synchronized (celestialBodyMap) {
            CelestialBody body = celestialBodyMap.get(name);
            if (body == null) {
                synchronized (loadersMap) {
                    List<CelestialBodyLoader> loaders = loadersMap.get(name);
                    if (loaders == null || loaders.isEmpty()) {
                        addDefaultCelestialBodyLoader(name, JPLEphemeridesLoader.DEFAULT_DE_SUPPORTED_NAMES);
                        addDefaultCelestialBodyLoader(name, JPLEphemeridesLoader.DEFAULT_INPOP_SUPPORTED_NAMES);
                        loaders = loadersMap.get(name);
                    }
                    OrekitException delayedException = null;
                    for (CelestialBodyLoader loader : loaders) {
                        try {
                            body = loader.loadCelestialBody(name);
                            if (body != null) {
                                break;
                            }
                        } catch (OrekitException oe) {
                            delayedException = oe;
                        }
                    }
                    if (body == null) {
                        throw (delayedException != null) ?
                                delayedException :
                                new OrekitException(OrekitMessages.NO_DATA_LOADED_FOR_CELESTIAL_BODY, name);
                    }

                }

                // save the body
                celestialBodyMap.put(name, body);

            }

            return body;

        }
    }

}