# MIT License
#
# Copyright (c) 2017 Satellogic SA
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import datetime as dt
import logging
import warnings
from collections import namedtuple
from math import pi, acos, degrees, radians

import numpy as np
try:
    from scipy.optimize import brentq, minimize_scalar
except ImportError:
    warnings.warn('scipy module was not found, some features may not work properly.',
                  ImportWarning)

from orbit_predictor.constants import MU_E
from orbit_predictor.exceptions import NotReachable, PropagationError
from orbit_predictor import coordinate_systems
from orbit_predictor.keplerian import rv2coe
from orbit_predictor.utils import (
    angle_between,
    cross_product,
    dot_product,
    reify,
    vector_diff,
    vector_norm,
    gstime_from_datetime,
    get_shadow,
    get_sun,
    eclipse_duration,
    get_satellite_minus_penumbra_verticals,
)

logger = logging.getLogger(__name__)


ONE_SECOND = dt.timedelta(seconds=1)


def round_datetime(dt_):
    return dt_


class Position(namedtuple(
        "Position", ['when_utc', 'position_ecef', 'velocity_ecef', 'error_estimate'])):

    @reify
    def position_llh(self):
        """Latitude (deg), longitude (deg), altitude (km)."""
        return coordinate_systems.ecef_to_llh(self.position_ecef)

    @reify
    def osculating_elements(self):
        """Osculating Keplerian orbital elements.

        Semimajor axis (km), eccentricity, inclination (deg),
        right ascension of the ascending node or RAAN (deg),
        argument of perigee (deg), true anomaly (deg).

        """
        gmst = gstime_from_datetime(self.when_utc)
        position_eci = coordinate_systems.ecef_to_eci(self.position_ecef, gmst)
        velocity_eci = coordinate_systems.ecef_to_eci(self.velocity_ecef, gmst)

        # Convert position to Keplerian osculating elements
        p, ecc, inc, raan, argp, ta = rv2coe(
            MU_E, np.array(position_eci), np.array(velocity_eci)
        )
        # Transform to more familiar semimajor axis
        sma = p / (1 - ecc ** 2)

        return sma, ecc, degrees(inc), degrees(raan), degrees(argp), degrees(ta)


class PredictedPass:
    def __init__(self, location, sate_id,
                 max_elevation_deg,
                 aos, los, duration_s,
                 max_elevation_position=None,
                 max_elevation_date=None):
        self.location = location
        self.sate_id = sate_id
        self.max_elevation_position = max_elevation_position
        self.max_elevation_date = max_elevation_date
        self.max_elevation_deg = max_elevation_deg
        self.aos = aos
        self.los = los
        self.duration_s = duration_s

    @property
    def midpoint(self):
        """Returns a datetime of the midpoint of the pass"""
        return self.aos + (self.los - self.aos) / 2

    def __repr__(self):
        return "<PredictedPass {} over {} on {}>".format(self.sate_id, self.location, self.aos)

    def __eq__(self, other):
        return all([issubclass(other.__class__, PredictedPass),
                    self.location == other.location,
                    self.sate_id == other.sate_id,
                    self.max_elevation_position == other.max_elevation_position,
                    self.max_elevation_date == other.max_elevation_date,
                    self.max_elevation_deg == other.max_elevation_deg,
                    self.aos == other.aos,
                    self.los == other.los,
                    self.duration_s == other.duration_s])

    def get_off_nadir_angle(self):
        warnings.warn("This method is deprecated!", DeprecationWarning)
        return self.off_nadir_deg

    @reify
    def off_nadir_deg(self):
        """Computes off-nadir angle calculation

        Given satellite position ``sate_pos``, velocity ``sate_vel``, and
        location ``target`` in a common frame, off-nadir angle ``off_nadir_angle``
        is given by:
            t2b = sate_pos - target
            cos(off_nadir_angle) =     (sate_pos  · t2b)     # Vectorial dot product
                                    _______________________
                                    || sate_pos || || t2b||

        Sign for the rotation is calculated this way

        cross = target ⨯ sate_pos
        sign =   cross · sate_vel
               ____________________
               | cross · sate_vel |
        """
        sate_pos = self.max_elevation_position.position_ecef
        sate_vel = self.max_elevation_position.velocity_ecef
        target = self.location.position_ecef
        t2b = vector_diff(sate_pos, target)
        angle = acos(
            dot_product(sate_pos, t2b) / (vector_norm(sate_pos) * vector_norm(t2b))
        )

        cross = cross_product(target, sate_pos)
        dot = dot_product(cross, sate_vel)
        try:
            sign = dot / abs(dot)
        except ZeroDivisionError:
            sign = 1

        return degrees(angle) * sign


class Predictor:

    @property
    def sate_id(self):
        raise NotImplementedError

    def propagate_eci(self, when_utc=None):
        raise NotImplementedError

    def get_position(self, when_utc=None):
        raise NotImplementedError("You have to implement it!")

    def get_shadow(self, when_utc=None):
        """Gives illumination at given time (2 for illuminated, 1 for penumbra, 0 for umbra)."""
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        return get_shadow(
            self.get_position(when_utc).position_ecef,
            when_utc
        )

    def get_normal_vector(self, when_utc=None):
        """Gets unitary normal vector (orthogonal to orbital plane) at given time."""
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        position, velocity = self.propagate_eci(when_utc)
        orbital_plane_normal = np.cross(position, velocity)
        return orbital_plane_normal / vector_norm(orbital_plane_normal)

    def get_beta(self, when_utc=None):
        """Gets angle between orbital plane and Sun direction (beta) at given time, in degrees."""
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        # Here we calculate the complementary angle of beta,
        # because we use the normal vector of the orbital plane
        beta_comp = angle_between(
            get_sun(when_utc),
            self.get_normal_vector(when_utc)
        )

        # We subtract from 90 degrees to return the real beta angle
        return 90 - beta_comp


class CartesianPredictor(Predictor):

    def _propagate_ecef(self, when_utc=None):
        """Return position and velocity in the given date using ECEF coordinate system."""
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        position_eci, velocity_eci = self.propagate_eci(when_utc)
        gmst = gstime_from_datetime(when_utc)
        position_ecef = coordinate_systems.eci_to_ecef(position_eci, gmst)
        velocity_ecef = coordinate_systems.eci_to_ecef(velocity_eci, gmst)
        return position_ecef, velocity_ecef

    @reify
    def mean_motion(self):
        """Mean motion, in radians per minute"""
        raise NotImplementedError

    @reify
    def period(self):
        """Orbital period, in minutes"""
        return 2 * pi / self.mean_motion

    def get_position(self, when_utc=None):
        """Return a Position namedtuple in ECEF coordinate system"""
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        position_ecef, velocity_ecef = self._propagate_ecef(when_utc)

        return Position(when_utc=when_utc, position_ecef=position_ecef,
                        velocity_ecef=velocity_ecef, error_estimate=None)

    def get_only_position(self, when_utc=None):
        """Return a tuple in ECEF coordinate system"""
        return self.get_position(when_utc).position_ecef

    def get_eclipse_duration(self, when_utc=None, tolerance=1e-1):
        """Gets eclipse duration at given time, in minutes"""
        ecc = self.get_position(when_utc).osculating_elements[1]
        if ecc > tolerance:
            raise NotImplementedError("Non circular orbits are not supported")

        beta = self.get_beta(when_utc)
        return eclipse_duration(beta, self.period)

    def passes_over(self, location, when_utc, limit_date=None, max_elevation_gt=0, aos_at_dg=0):
        return LocationPredictor(location, self, when_utc, limit_date,
                                 max_elevation_gt, aos_at_dg)

    def get_next_pass(self, location, when_utc=None, max_elevation_gt=5,
                      aos_at_dg=0, limit_date=None):
        """Return a PredictedPass instance with the data of the next pass over the given location

        location_llh: point on Earth we want to see from the satellite.
        when_utc: datetime UTC after which the pass is calculated, default to now.
        max_elevation_gt: filter passes with max_elevation under it.
        aos_at_dg: This is if we want to start the pass at a specific elevation.

        The next pass with a LOS strictly after when_utc will be returned,
        possibly the current pass.
        """
        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        for pass_ in self.passes_over(location, when_utc, limit_date,
                                      max_elevation_gt=max_elevation_gt,
                                      aos_at_dg=aos_at_dg):
            return pass_
        else:
            raise NotReachable('Propagation limit date exceeded')

    def eclipses_since(self, when_utc=None, limit_date=None):
        """
        An iterator that yields all eclipses start and end times between
        when_utc and limit_date.

        The next eclipse with a end strictly after when_utc will be returned,
        possibly the current eclipse.
        The last eclipse returned starts before limit_date, but it can end
        strictly after limit_date.
        No circular orbits are not supported, and will raise NotImplementedError.
        """
        def _get_illumination(t):
            my_start = start + dt.timedelta(seconds=t)
            result = get_satellite_minus_penumbra_verticals(
                self.get_only_position(my_start),
                my_start
            )
            return result

        if when_utc is None:
            when_utc = dt.datetime.utcnow()

        orbital_period_s = self.period * 60
        # A third of the orbit period is used as the base window of the search.
        # This window ensures the function get_satellite_minus_penumbra_verticals
        # will not have more than one local minimum (one in the illuminated phase and
        # the other in penumbra).
        base_search_window_s = orbital_period_s / 3
        start = when_utc

        while limit_date is None or start < limit_date:

            # a minimum negative value is aproximatelly the middle point of the eclipse
            minimum_illumination = minimize_scalar(
                _get_illumination,
                bounds=(0, base_search_window_s),
                method="bounded",
                options={"xatol": 1e-2},
            )
            eclipse_center_candidate_delta_s = minimum_illumination.x

            # If found a minimum that is not illuminated, there is an eclipse here
            if _get_illumination(eclipse_center_candidate_delta_s) < 0:
                # The small time interval to search zeros around the center
                # is estimated with the expected eclipse duration (which generally
                # is smaller than expected, and that is the reason of the 1.5 coeficient).
                # Also a minimum of 180 seconds was added because
                # in some cases the estimation is 0 even though there is an eclipse.
                eclipse_duration_estimation_s = self.get_eclipse_duration(start) * 60
                zero_search_window_s = max(180, 1.5 * eclipse_duration_estimation_s)

                # Search now both zeros to get the start and end of the eclipse
                eclipse_start_delta_s = brentq(
                    _get_illumination,
                    eclipse_center_candidate_delta_s - zero_search_window_s,
                    eclipse_center_candidate_delta_s,
                    xtol=1e-2,
                    full_output=False,
                )
                eclipse_end_delta_s = brentq(
                    _get_illumination,
                    eclipse_center_candidate_delta_s,
                    eclipse_center_candidate_delta_s + zero_search_window_s,
                    xtol=1e-2,
                    full_output=False,
                )
                eclipse_start = start + dt.timedelta(seconds=eclipse_start_delta_s)
                eclipse_end = start + dt.timedelta(seconds=eclipse_end_delta_s)
                yield eclipse_start, eclipse_end
                start = eclipse_end + dt.timedelta(seconds=base_search_window_s)
            else:
                start += dt.timedelta(seconds=base_search_window_s)


class GPSPredictor(Predictor):
    pass


class LocationPredictor:
    """Predicts passes over a given location
    Exposes an iterable interface
    """

    def __init__(self, location, predictor, start_date, limit_date=None,
                 max_elevation_gt=0, aos_at_dg=0, *, propagator=None):
        if propagator is not None:
            warnings.warn(
                "propagator parameter was renamed to predictor "
                "and will be removed in a future release",
                DeprecationWarning
            )
            predictor = propagator

        self.location = location
        self.predictor = predictor
        self.start_date = start_date
        self.limit_date = limit_date

        self.max_elevation_gt = radians(max([max_elevation_gt, aos_at_dg]))
        self.aos_at = radians(aos_at_dg)

    @property
    def propagator(self):
        warnings.warn(
            "propagator parameter was renamed to predictor "
            "and will be removed in a future release",
            DeprecationWarning
        )
        return self.predictor

    def __iter__(self):
        """Returns one pass each time"""
        current_date = self.start_date
        while True:
            if self.is_ascending(current_date):
                # we need a descending point
                ascending_date = current_date
                descending_date = self._find_nearest_descending(ascending_date)
                pass_ = self._refine_pass(ascending_date, descending_date)
                if pass_.valid:
                    if self.limit_date is not None and pass_.aos > self.limit_date:
                        break
                    yield self._build_predicted_pass(pass_)

                if self.limit_date is not None and current_date > self.limit_date:
                    break

                current_date = pass_.tca + self._orbit_step(0.6)

            else:
                current_date = self._find_nearest_ascending(current_date)

    def _build_predicted_pass(self, accuratepass):
        """Returns a classic predicted pass"""
        tca_position = self.predictor.get_position(accuratepass.tca)

        return PredictedPass(self.location, self.predictor.sate_id,
                             max_elevation_deg=accuratepass.max_elevation_deg,
                             aos=accuratepass.aos,
                             los=accuratepass.los,
                             duration_s=accuratepass.duration.total_seconds(),
                             max_elevation_position=tca_position,
                             max_elevation_date=accuratepass.tca,
                             )

    def _find_nearest_descending(self, ascending_date):
        for candidate in self._sample_points(ascending_date):
            if not self.is_ascending(candidate):
                return candidate
        else:
            logger.error('Could not find a descending pass over %s start date: %s - TLE: %s',
                         self.location, ascending_date, self.predictor.tle)
            raise PropagationError("Can not find an descending phase")

    def _find_nearest_ascending(self, descending_date):
        for candidate in self._sample_points(descending_date):
            if self.is_ascending(candidate):
                return candidate
        else:
            logger.error('Could not find an ascending pass over %s start date: %s - TLE: %s',
                         self.location, descending_date, self.predictor.tle)
            raise PropagationError('Can not find an ascending phase')

    def _sample_points(self, date):
        """Helper method to found ascending or descending phases of elevation"""
        start = date
        end = date + self._orbit_step(0.99)
        mid = self.midpoint(start, end)
        mid_right = self.midpoint(mid, end)
        mid_left = self.midpoint(start, mid)

        return [end, mid, mid_right, mid_left]

    def _refine_pass(self, ascending_date, descending_date):
        tca = self._find_tca(ascending_date, descending_date)
        elevation = self._elevation_at(tca)

        if elevation > self.max_elevation_gt:
            aos = self._find_aos(tca)
            los = self._find_los(tca)
        else:
            aos = los = None

        return AccuratePredictedPass(aos, tca, los, elevation)

    def _find_tca(self, ascending_date, descending_date):
        while not self._precision_reached(ascending_date, descending_date):
            midpoint = self.midpoint(ascending_date, descending_date)
            if self.is_ascending(midpoint):
                ascending_date = midpoint
            else:
                descending_date = midpoint

        return ascending_date

    def _precision_reached(self, start, end):
        # TODO: Allow the precision to change from the outside
        return end - start <= ONE_SECOND

    @staticmethod
    def midpoint(start, end):
        """Returns the midpoint between two dates"""
        return start + (end - start) / 2

    def _elevation_at(self, when_utc):
        position = self.predictor.get_only_position(when_utc)
        return self.location.elevation_for(position)

    def is_passing(self, when_utc):
        """Returns a boolean indicating if satellite is actually visible"""
        return bool(self._elevation_at(when_utc))

    def is_ascending(self, when_utc):
        """Check is elevation is ascending or descending on a given point"""
        elevation = self._elevation_at(when_utc)
        next_elevation = self._elevation_at(when_utc + ONE_SECOND)
        return elevation <= next_elevation

    def _orbit_step(self, size):
        """Returns a time step, that will make the satellite advance a given number of orbits"""
        step_in_radians = size * 2 * pi
        seconds = (step_in_radians / self.predictor.mean_motion) * 60
        return dt.timedelta(seconds=seconds)

    def _find_aos(self, tca):
        end = tca
        start = tca - self._orbit_step(0.34)  # On third of the orbit
        elevation = self._elevation_at(start)
        assert elevation < 0
        while not self._precision_reached(start, end):
            midpoint = self.midpoint(start, end)
            elevation = self._elevation_at(midpoint)
            if elevation < self.aos_at:
                start = midpoint
            else:
                end = midpoint
        return end

    def _find_los(self, tca):
        start = tca
        end = tca + self._orbit_step(0.34)
        while not self._precision_reached(start, end):
            midpoint = self.midpoint(start, end)
            elevation = self._elevation_at(midpoint)

            if elevation < self.aos_at:
                end = midpoint
            else:
                start = midpoint

        return start


class AccuratePredictedPass:

    def __init__(self, aos, tca, los, max_elevation):
        self.aos = round_datetime(aos) if aos is not None else None
        self.tca = round_datetime(tca)
        self.los = round_datetime(los) if los is not None else None
        self.max_elevation = max_elevation

    @property
    def valid(self):
        return self.max_elevation > 0 and self.aos is not None and self.los is not None

    @reify
    def max_elevation_deg(self):
        return degrees(self.max_elevation)

    @reify
    def duration(self):
        return self.los - self.aos

[((54, 9, 54, 36), 'logging.getLogger', 'logging.getLogger', ({(54, 27, 54, 35): '__name__'}, {}), '(__name__)', False, 'import logging\n'), ((57, 13, 57, 36), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((64, 15, 65, 85), 'collections.namedtuple', 'namedtuple', ({(65, 8, 65, 18): '"""Position"""', (65, 20, 65, 84): "['when_utc', 'position_ecef', 'velocity_ecef', 'error_estimate']"}, {}), "('Position', ['when_utc', 'position_ecef', 'velocity_ecef',\n    'error_estimate'])", False, 'from collections import namedtuple\n'), ((33, 4, 34, 32), 'warnings.warn', 'warnings.warn', ({(33, 18, 33, 84): '"""scipy module was not found, some features may not work properly."""', (34, 18, 34, 31): 'ImportWarning'}, {}), "(\n    'scipy module was not found, some features may not work properly.',\n    ImportWarning)", False, 'import warnings\n'), ((70, 15, 70, 65), 'orbit_predictor.coordinate_systems.ecef_to_llh', 'coordinate_systems.ecef_to_llh', ({(70, 46, 70, 64): 'self.position_ecef'}, {}), '(self.position_ecef)', False, 'from orbit_predictor import coordinate_systems\n'), ((81, 15, 81, 50), 'orbit_predictor.utils.gstime_from_datetime', 'gstime_from_datetime', ({(81, 36, 81, 49): 'self.when_utc'}, {}), '(self.when_utc)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((82, 23, 82, 79), 'orbit_predictor.coordinate_systems.ecef_to_eci', 'coordinate_systems.ecef_to_eci', ({(82, 54, 82, 72): 'self.position_ecef', (82, 74, 82, 78): 'gmst'}, {}), '(self.position_ecef, gmst)', False, 'from orbit_predictor import coordinate_systems\n'), ((83, 23, 83, 79), 'orbit_predictor.coordinate_systems.ecef_to_eci', 'coordinate_systems.ecef_to_eci', ({(83, 54, 83, 72): 'self.velocity_ecef', (83, 74, 83, 78): 'gmst'}, {}), '(self.velocity_ecef, gmst)', False, 'from orbit_predictor import coordinate_systems\n'), ((130, 8, 130, 71), 'warnings.warn', 'warnings.warn', ({(130, 22, 130, 50): '"""This method is deprecated!"""', (130, 52, 130, 70): 'DeprecationWarning'}, {}), "('This method is deprecated!', DeprecationWarning)", False, 'import warnings\n'), ((155, 14, 155, 43), 'orbit_predictor.utils.vector_diff', 'vector_diff', ({(155, 26, 155, 34): 'sate_pos', (155, 36, 155, 42): 'target'}, {}), '(sate_pos, target)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((160, 16, 160, 47), 'orbit_predictor.utils.cross_product', 'cross_product', ({(160, 30, 160, 36): 'target', (160, 38, 160, 46): 'sate_pos'}, {}), '(target, sate_pos)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((161, 14, 161, 42), 'orbit_predictor.utils.dot_product', 'dot_product', ({(161, 26, 161, 31): 'cross', (161, 33, 161, 41): 'sate_vel'}, {}), '(cross, sate_vel)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((198, 31, 198, 59), 'numpy.cross', 'np.cross', ({(198, 40, 198, 48): 'position', (198, 50, 198, 58): 'velocity'}, {}), '(position, velocity)', True, 'import numpy as np\n'), ((225, 15, 225, 45), 'orbit_predictor.utils.gstime_from_datetime', 'gstime_from_datetime', ({(225, 36, 225, 44): 'when_utc'}, {}), '(when_utc)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((226, 24, 226, 74), 'orbit_predictor.coordinate_systems.eci_to_ecef', 'coordinate_systems.eci_to_ecef', ({(226, 55, 226, 67): 'position_eci', (226, 69, 226, 73): 'gmst'}, {}), '(position_eci, gmst)', False, 'from orbit_predictor import coordinate_systems\n'), ((227, 24, 227, 74), 'orbit_predictor.coordinate_systems.eci_to_ecef', 'coordinate_systems.eci_to_ecef', ({(227, 55, 227, 67): 'velocity_eci', (227, 69, 227, 73): 'gmst'}, {}), '(velocity_eci, gmst)', False, 'from orbit_predictor import coordinate_systems\n'), ((261, 15, 261, 50), 'orbit_predictor.utils.eclipse_duration', 'eclipse_duration', ({(261, 32, 261, 36): 'beta', (261, 38, 261, 49): 'self.period'}, {}), '(beta, self.period)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((388, 22, 388, 40), 'math.radians', 'radians', ({(388, 30, 388, 39): 'aos_at_dg'}, {}), '(aos_at_dg)', False, 'from math import pi, acos, degrees, radians\n'), ((392, 8, 396, 9), 'warnings.warn', 'warnings.warn', ({(393, 12, 394, 53): '"""propagator parameter was renamed to predictor and will be removed in a future release"""', (395, 12, 395, 30): 'DeprecationWarning'}, {}), "(\n    'propagator parameter was renamed to predictor and will be removed in a future release'\n    , DeprecationWarning)", False, 'import warnings\n'), ((511, 15, 511, 44), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((556, 15, 556, 42), 'math.degrees', 'degrees', ({(556, 23, 556, 41): 'self.max_elevation'}, {}), '(self.max_elevation)', False, 'from math import pi, acos, degrees, radians\n'), ((87, 18, 87, 40), 'numpy.array', 'np.array', ({(87, 27, 87, 39): 'position_eci'}, {}), '(position_eci)', True, 'import numpy as np\n'), ((87, 42, 87, 64), 'numpy.array', 'np.array', ({(87, 51, 87, 63): 'velocity_eci'}, {}), '(velocity_eci)', True, 'import numpy as np\n'), ((92, 25, 92, 37), 'math.degrees', 'degrees', ({(92, 33, 92, 36): 'inc'}, {}), '(inc)', False, 'from math import pi, acos, degrees, radians\n'), ((92, 39, 92, 52), 'math.degrees', 'degrees', ({(92, 47, 92, 51): 'raan'}, {}), '(raan)', False, 'from math import pi, acos, degrees, radians\n'), ((92, 54, 92, 67), 'math.degrees', 'degrees', ({(92, 62, 92, 66): 'argp'}, {}), '(argp)', False, 'from math import pi, acos, degrees, radians\n'), ((92, 69, 92, 80), 'math.degrees', 'degrees', ({(92, 77, 92, 79): 'ta'}, {}), '(ta)', False, 'from math import pi, acos, degrees, radians\n'), ((167, 15, 167, 29), 'math.degrees', 'degrees', ({(167, 23, 167, 28): 'angle'}, {}), '(angle)', False, 'from math import pi, acos, degrees, radians\n'), ((185, 23, 185, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((195, 23, 195, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((199, 38, 199, 71), 'orbit_predictor.utils.vector_norm', 'vector_norm', ({(199, 50, 199, 70): 'orbital_plane_normal'}, {}), '(orbital_plane_normal)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((204, 23, 204, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((209, 12, 209, 29), 'orbit_predictor.utils.get_sun', 'get_sun', ({(209, 20, 209, 28): 'when_utc'}, {}), '(when_utc)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((222, 23, 222, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((243, 23, 243, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((280, 23, 280, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((287, 18, 287, 65), 'orbit_predictor.exceptions.NotReachable', 'NotReachable', ({(287, 31, 287, 64): '"""Propagation limit date exceeded"""'}, {}), "('Propagation limit date exceeded')", False, 'from orbit_predictor.exceptions import NotReachable, PropagationError\n'), ((309, 23, 309, 43), 'datetime.datetime.utcnow', 'dt.datetime.utcnow', ({}, {}), '()', True, 'import datetime as dt\n'), ((322, 35, 327, 13), 'scipy.optimize.minimize_scalar', 'minimize_scalar', (), '', False, 'from scipy.optimize import brentq, minimize_scalar\n'), ((375, 12, 379, 13), 'warnings.warn', 'warnings.warn', ({(376, 16, 377, 57): '"""propagator parameter was renamed to predictor and will be removed in a future release"""', (378, 16, 378, 34): 'DeprecationWarning'}, {}), "(\n    'propagator parameter was renamed to predictor and will be removed in a future release'\n    , DeprecationWarning)", False, 'import warnings\n'), ((441, 18, 441, 70), 'orbit_predictor.exceptions.PropagationError', 'PropagationError', ({(441, 35, 441, 69): '"""Can not find an descending phase"""'}, {}), "('Can not find an descending phase')", False, 'from orbit_predictor.exceptions import NotReachable, PropagationError\n'), ((450, 18, 450, 69), 'orbit_predictor.exceptions.PropagationError', 'PropagationError', ({(450, 35, 450, 68): '"""Can not find an ascending phase"""'}, {}), "('Can not find an ascending phase')", False, 'from orbit_predictor.exceptions import NotReachable, PropagationError\n'), ((157, 12, 157, 38), 'orbit_predictor.utils.dot_product', 'dot_product', ({(157, 24, 157, 32): 'sate_pos', (157, 34, 157, 37): 't2b'}, {}), '(sate_pos, t2b)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((301, 31, 301, 54), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((341, 40, 347, 17), 'scipy.optimize.brentq', 'brentq', (), '', False, 'from scipy.optimize import brentq, minimize_scalar\n'), ((348, 38, 354, 17), 'scipy.optimize.brentq', 'brentq', (), '', False, 'from scipy.optimize import brentq, minimize_scalar\n'), ((360, 25, 360, 67), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((157, 42, 157, 63), 'orbit_predictor.utils.vector_norm', 'vector_norm', ({(157, 54, 157, 62): 'sate_pos'}, {}), '(sate_pos)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((157, 66, 157, 82), 'orbit_predictor.utils.vector_norm', 'vector_norm', ({(157, 78, 157, 81): 't2b'}, {}), '(t2b)', False, 'from orbit_predictor.utils import angle_between, cross_product, dot_product, reify, vector_diff, vector_norm, gstime_from_datetime, get_shadow, get_sun, eclipse_duration, get_satellite_minus_penumbra_verticals\n'), ((355, 40, 355, 83), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((356, 38, 356, 79), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n'), ((358, 38, 358, 80), 'datetime.timedelta', 'dt.timedelta', (), '', True, 'import datetime as dt\n')]