pbrod/Nvector

View on GitHub
src/nvector/core.py

Summary

Maintainability
B
6 hrs
Test Coverage
"""
Core geodesic functions
=======================
This file is part of NavLab and is available from www.navlab.net/nvector

"""
# pylint: disable=invalid-name

from __future__ import division, print_function
import warnings
import numpy as np
from numpy import arctan2, sin, cos, cross, dot, sqrt
from numpy.linalg import norm
from scipy.interpolate import interp1d
from scipy.signal import savgol_filter
from nvector import _examples, license as _license
from nvector.rotation import E_rotation, n_E2R_EN, n_E2lat_lon, change_axes_to_E
from nvector.util import mdot, nthroot, unit, eccentricity2, polar_radius
from nvector.karney import (geodesic_reckon as _geodesic_reckon,
                            geodesic_distance as _geodesic_distance)
from nvector._common import test_docstrings, use_docstring, _make_summary


__all__ = ['closest_point_on_great_circle',
           'cross_track_distance',
           'course_over_ground',
           'euclidean_distance',
           'geodesic_distance',
           'geodesic_reckon',
           'great_circle_distance',
           'great_circle_distance_rad',
           'great_circle_normal',
           'interp_nvectors',
           'interpolate',
           'interp_nvectors',
           'intersect',
           'mean_horizontal_position',
           'lat_lon2n_E',
           'n_E2lat_lon',
           'n_EA_E_and_n_EB_E2p_AB_E',
           'n_EA_E_and_n_EB_E2p_AB_N',
           'n_EA_E_and_p_AB_E2n_EB_E',
           'n_EA_E_and_p_AB_N2n_EB_E',
           'n_EB_E2p_EB_E',
           'p_EB_E2n_EB_E',
           'n_EA_E_distance_and_azimuth2n_EB_E',
           'n_EA_E_and_n_EB_E2azimuth',
           'on_great_circle',
           'on_great_circle_path',
           ]


def lat_lon2n_E(latitude, longitude, R_Ee=None):
    """
    Converts latitude and longitude to n-vector.

    Parameters
    ----------
    latitude, longitude: real scalars or vectors of length n.
        Geodetic latitude and longitude given in [rad]
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    n_E: 3 x n array
        n-vector(s) [no unit] decomposed in E.

    Examples
    --------
    >>> import nvector as nv
    >>> pi = 3.141592653589793

    Scalar call
    >>> nv.allclose(nv.lat_lon2n_E(0, 0), [[1.],
    ...                                    [0.],
    ...                                    [0.]])
    True

    Vectorized call
    >>> nv.allclose(nv.lat_lon2n_E([0., 0.], [0., pi/2]), [[1., 0.],
    ...                                                   [0., 1.],
    ...                                                   [0., 0.]])
    True

    Broadcasting call
    >>> nv.allclose(nv.lat_lon2n_E(0., [0, pi/2]), [[1., 0.],
    ...                                           [0., 1.],
    ...                                           [0., 0.]])
    True

    See also
    --------
    n_E2lat_lon
    """
    if R_Ee is None:
        R_Ee = E_rotation()
    # Equation (3) from Gade (2010):  n-vector decomposed in E with axes='e'
    n_e = np.vstack((sin(latitude) * np.ones_like(longitude),
                     cos(latitude) * sin(longitude),
                     -cos(latitude) * cos(longitude)))
    # n_E = dot(R_Ee.T, n_e)
    n_E = np.matmul(R_Ee.T, n_e)  # n-vector decomposed in E with axes 'E'
    return n_E


@use_docstring(_examples.get_examples_no_header([4], oo_solution=False))
def n_EB_E2p_EB_E(n_EB_E, depth=0, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Converts n-vector to Cartesian position vector in meters.

    Parameters
    ----------
    n_EB_E:  3 x m array
        n-vector(s) [no unit] of position B, decomposed in E.
    depth:  1 x n array
        Depth(s) [m] of system B, relative to the ellipsoid (depth = -height)
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    p_EB_E:  3 x max(m,n) array
        Cartesian position vector(s) [m] from E to B, decomposed in E.

    Notes
    -----
    The position of B (typically body) relative to E (typically Earth) is
    given into this function as n-vector, `n_EB_E`. The function converts
    to cartesian position vector ("ECEF-vector"), `p_EB_E`, in meters.
    The calculation is exact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.
    The shape of the output `p_EB_E` is the broadcasted shapes of `n_EB_E`
    and `depth`.

    Examples
    --------
    {super}

    See also
    --------
    p_EB_E2n_EB_E, n_EA_E_and_p_AB_E2n_EB_E, n_EA_E_and_n_EB_E2p_AB_E
    """
    if R_Ee is None:
        R_Ee = E_rotation()

    #     n_EB_E = np.atleast_2d(n_EB_E)
    #     _nvector_check_length(n_EB_E)
    #     n_EB_e = unit(np.matmul(R_Ee, n_EB_E))

    # Make sure to rotate the coordinates so that:
    # x -> north pole and yz-plane coincides with the equatorial
    # plane before using equation 22!
    n_EB_e = change_axes_to_E(n_EB_E, R_Ee)
    b = polar_radius(a, f)  # semi-minor axis

    # The following code implements equation (22) in Gade (2010):
    scale = np.vstack((1,
                       (1 - f),
                       (1 - f)))
    denominator = norm(n_EB_e / scale, axis=0, keepdims=True)

    # We first calculate the position at the origin of coordinate system L,
    # which has the same n-vector as B (n_EL_e = n_EB_e),
    # but lies at the surface of the Earth (z_EL = 0).

    p_EL_e = b / denominator * n_EB_e / scale**2
    # rotate back to the original coordinate system
    p_EB_E = np.matmul(R_Ee.T, p_EL_e - n_EB_e * depth)

    return p_EB_E


def _compute_k(a, e_2, q, Ryz_2):
    """Returns the k value in equation (23) from Gade (2010)"""
    p = Ryz_2 / a ** 2
    r = (p + q - e_2 ** 2) / 6
    s = e_2 ** 2 * p * q / (4 * r ** 3)
    t = nthroot((1 + s + sqrt(s * (2 + s))), 3)
    # t = (1 + s + sqrt(s * (2 + s)))**(1. / 3)
    u = r * (1 + t + 1. / t)
    v = sqrt(u ** 2 + e_2 ** 2 * q)
    w = e_2 * (u + v - q) / (2 * v)
    return sqrt(u + v + w ** 2) - w


def _equation23(a, f, p_EB_E):
    """equation (23) from Gade (2010)"""
    Ryz_2 = p_EB_E[1, :]**2 + p_EB_E[2, :]**2
    Rx_2 = p_EB_E[0, :]**2
    e_2 = eccentricity2(f)[0]
    q = (1 - e_2) / (a ** 2) * Rx_2
    Ryz = sqrt(Ryz_2)  # Ryz = component of p_EB_E in the equatorial plane
    k = _compute_k(a, e_2, q, Ryz_2)
    d = k * Ryz / (k + e_2)
    temp0 = sqrt(d ** 2 + Rx_2)
    height = (k + e_2 - 1) / k * temp0  # Calculate height:
    x_scale = 1. / temp0
    yz_scale = x_scale * k / (k + e_2)
    return x_scale, yz_scale, -height


@use_docstring(_examples.get_examples_no_header([3], oo_solution=False))
def p_EB_E2n_EB_E(p_EB_E, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Converts Cartesian position vector in meters to n-vector.

    Parameters
    ----------
    p_EB_E:  3 x n array
        Cartesian position vector(s) [m] from E to B, decomposed in E.
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    n_EB_E:  3 x n array
        n-vector(s) [no unit] of position B, decomposed in E.
    depth:  1 x n array
        Depth(s) [m] of system B, relative to the ellipsoid (depth = -height)

    Notes
    -----
    The position of B (typically body) relative to E (typically Earth) is
    given into this function as cartesian position vector `p_EB_E`, in meters.
    ("ECEF-vector"). The function converts to n-vector, `n_EB_E` and its `depth`.
    The calculation is excact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.

    Examples
    --------
    {super}

    See also
    --------
    n_EB_E2p_EB_E, n_EA_E_and_p_AB_E2n_EB_E, n_EA_E_and_n_EB_E2p_AB_E

    """
    if R_Ee is None:
        # R_Ee selects correct E-axes, see E_rotation for details
        R_Ee = E_rotation()

    # Make sure to rotate the coordinates so that:
    # x -> north pole and yz-plane coincides with the equatorial
    # plane before using equation 23!
    p_EB_e = np.matmul(R_Ee, p_EB_E)

    # The following code implements equation (23) from Gade (2010):
    x_scale, yz_scale, depth = _equation23(a, f, p_EB_e)

    n_EB_e_x = x_scale * p_EB_e[0, :]
    n_EB_e_y = yz_scale * p_EB_e[1, :]
    n_EB_e_z = yz_scale * p_EB_e[2, :]

    n_EB_e = np.vstack((n_EB_e_x, n_EB_e_y, n_EB_e_z))
    # Rotate back to the original coordinate system.
    n_EB_E = unit(np.matmul(R_Ee.T, n_EB_e))  # Ensure unit length

    return n_EB_E, depth


@use_docstring(_examples.get_examples_no_header([1], False))
def n_EA_E_and_n_EB_E2p_AB_E(n_EA_E, n_EB_E, z_EA=0, z_EB=0, a=6378137,
                             f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns the delta vector from position A to B decomposed in E.

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x j  and 3 x k arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.
    z_EA, z_EB:  3 x m  and 3 x n arrays
        Depth(s) [m] of system A and B, relative to the ellipsoid.
        (z_EA = -height, z_EB = -height)
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    p_AB_E:  3 x max(j,k,m,n) array
        Cartesian position vector(s) [m] from A to B, decomposed in E.

    Notes
    -----
    The n-vectors for positions A (`n_EA_E`) and B (`n_EB_E`) are given. The
    output is the delta vector from A to B decompose in E (`p_AB_E`).
    The calculation is excact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.
    The shape of the output `p_AB_E` is the broadcasted shapes of `n_EA_E`, `n_EB_E`,
    `z_EA` and `z_EB`.

    Examples
    --------
    {super}


    See also
    --------
    n_EA_E_and_p_AB_E2n_EB_E, n_EA_E_and_n_EB_E2p_AB_N, n_EB_E2p_EB_E,

    """

    # Function 1. in Section 5.4 in Gade (2010):
    p_EA_E = n_EB_E2p_EB_E(n_EA_E, z_EA, a, f, R_Ee)
    p_EB_E = n_EB_E2p_EB_E(n_EB_E, z_EB, a, f, R_Ee)
    p_AB_E = p_EB_E - p_EA_E
    return p_AB_E


@use_docstring(_examples.get_examples_no_header([1], False))
def n_EA_E_and_n_EB_E2p_AB_N(n_EA_E, n_EB_E, z_EA=0, z_EB=0, a=6378137,
                             f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns the delta vector from position A to B decomposed in N.

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x j and 3 x k arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.
    z_EA, z_EB:  3 x m and 3 x n arrays
        Depth(s) [m] of system A and B, relative to the ellipsoid.
        (z_EA = -height, z_EB = -height)
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    p_AB_N:  3 x max(j,k,m,n) array
        Cartesian position vector(s) [m] from A to B, decomposed in N.

    Notes
    -----
    The n-vectors for positions A (`n_EA_E`) and B (`n_EB_E`) are given. The
    output is the delta vector from A to B decomposed in N (`p_AB_N`).
    The calculation is excact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.
    The shape of the output p_AB_N is the broadcasted shapes of `n_EA_E`, `n_EB_E`,
    `z_EA` and `z_EB`.

    Examples
    --------
    {super}


    See also
    --------
    n_EA_E_and_p_AB_E2n_EB_E, p_EB_E2n_EB_E, n_EB_E2p_EB_E, n_EA_E_and_n_EB_E2p_AB_E

    """
    p_AB_E = n_EA_E_and_n_EB_E2p_AB_E(n_EA_E, n_EB_E, z_EA, z_EB, a, f, R_Ee)

    R_EN = n_E2R_EN(n_EA_E, R_Ee=R_Ee)

    # p_AB_N = dot(R_EN.T, p_AB_E)
    p_AB_N = mdot(np.swapaxes(R_EN, 1, 0), p_AB_E[:, None, ...]).reshape(3, -1)
    # (Note the transpose of R_EN: The "closest-rule" says that when
    # decomposing, the frame in the subscript of the rotation matrix that
    # is closest to the vector, should equal the frame where the vector is
    # decomposed. Thus the calculation np.dot(R_NE, p_AB_E) is correct,
    # since the vector is decomposed in E, and E is closest to the vector.
    # In the example we only had R_EN, and thus we must transpose it:
    # R_EN'=R_NE)
    return p_AB_N


@use_docstring(_examples.get_examples_no_header([2], oo_solution=False))
def n_EA_E_and_p_AB_E2n_EB_E(n_EA_E, p_AB_E, z_EA=0, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns position B from position A and delta vector decomposed in E.

    Parameters
    ----------
    n_EA_E:  3 x k array
        n-vector(s) [no unit] of position A, decomposed in E.
    p_AB_E:  3 x m array
        Cartesian position vector(s) [m] from A to B, decomposed in E.
    z_EA:  1 x n array
        Depth(s) [m] of system A, relative to the ellipsoid. (z_EA = -height)
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    n_EB_E:  3 x max(k,m,n) array
        n-vector(s) [no unit] of position B, decomposed in E.
    z_EB:  1 x max(k,m,n) array
        Depth(s) [m] of system B, relative to the ellipsoid.
        (z_EB = -height)

    Notes
    -----
    The n-vector for position A (`n_EA_E`) and the delta vector from position
    A to position B decomposed in E (`p_AB_E`) are given. The output is the
    n-vector of position B (`n_EB_E`) and depth of B (`z_EB`).
    The calculation is excact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.
    The shape of the output `n_EB_E` and `z_EB` is the broadcasted shapes of
    `n_EA_E`, `p_AB_E` and `z_EA`.

    Examples
    --------
    {super}

    See also
    --------
    n_EA_E_and_n_EB_E2p_AB_E, p_EB_E2n_EB_E, n_EB_E2p_EB_E
    """
    if R_Ee is None:
        R_Ee = E_rotation()
    n_EA_E, p_AB_E = np.atleast_2d(n_EA_E, p_AB_E)
    # Function 2. in Section 5.4 in Gade (2010):
    p_EA_E = n_EB_E2p_EB_E(n_EA_E, z_EA, a, f, R_Ee)
    p_EB_E = p_EA_E + p_AB_E
    n_EB_E, z_EB = p_EB_E2n_EB_E(p_EB_E, a, f, R_Ee)
    return n_EB_E, z_EB


@use_docstring(_examples.get_examples_no_header([2], oo_solution=False))
def n_EA_E_and_p_AB_N2n_EB_E(n_EA_E, p_AB_N, z_EA=0, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns position B from position A and delta vector decomposed in N.

    Parameters
    ----------
    n_EA_E:  3 x k array
        n-vector(s) [no unit] of position A, decomposed in E.
    p_AB_N:  3 x m array
        Cartesian position vector(s) [m] from A to B, decomposed in N.
    z_EA:  1 x n array
        Depth(s) [m] of system A, relative to the ellipsoid. (z_EA = -height)
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    n_EB_E:  3 x max(k,m,n) array
        n-vector(s) [no unit] of position B, decomposed in E.
    z_EB:  1 x max(k,m,n) array
        Depth(s) [m] of system B, relative to the ellipsoid.
        (z_EB = -height)

    Notes
    -----
    The n-vector for position A (n_EA_E) and the delta vector from position
    A to position B decomposed in N (p_AB_N) are given. The output is the
    n-vector of position B (`n_EB_E`) and depth of B (`z_EB`).
    The calculation is excact, taking the ellipsity of the Earth into account.
    It is also non-singular as both n-vector and p-vector are non-singular
    (except for the center of the Earth).
    The default ellipsoid model used is WGS-84, but other ellipsoids/spheres
    might be specified.
    The shape of the output `n_EB_E` and `z_EB` is the broadcasted shapes of
    `n_EA_E`, `p_AB_N` and `z_EA`.

    Examples
    --------
    {super}

    See also
    --------
    n_EA_E_and_n_EB_E2p_AB_N,
    n_EA_E_and_p_AB_E2n_EB_E,
    n_E2R_EN
    """
    if R_Ee is None:
        R_Ee = E_rotation()
    n_EA_E, p_AB_N = np.atleast_2d(n_EA_E, p_AB_N)

    R_EN = n_E2R_EN(n_EA_E, R_Ee=R_Ee)

    # p_AB_E = dot(R_EN, p_AB_N)
    p_AB_E = mdot(R_EN, p_AB_N[:, None, ...]).reshape(3, -1)

    return n_EA_E_and_p_AB_E2n_EB_E(n_EA_E, p_AB_E, z_EA, a=a, f=f, R_Ee=R_Ee)


def _interp_vectors(t_i, t, nvectors, kind, window_length, polyorder, mode, cval):
    if window_length > 0:
        window_length = window_length + (window_length + 1) % 2  # make sure it is an odd integer
        options = dict(axis=1, mode=mode, cval=cval)
        normals = savgol_filter(nvectors, window_length, polyorder, **options)
    else:
        normals = nvectors

    normal_i = interp1d(t, normals, axis=1, kind=kind, bounds_error=False)(t_i)
    return normal_i.reshape(nvectors.shape[0], -1)


def interp_nvectors(t_i, t, nvectors, kind='linear', window_length=0, polyorder=2, mode='interp',
                    cval=0.0):
    """
    Returns interpolated values from nvector data.

    Parameters
    ----------
    t_i: real vector of length m
        Vector of interpolation times.
    t: real vector of length n
        Vector of times.
    nvectors: 3 x n array
        n-vectors [no unit] decomposed in E.
    kind: str or int, optional
        Specifies the kind of interpolation as a string
        ('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'
        where 'zero', 'slinear', 'quadratic' and 'cubic' refer to a spline
        interpolation of zeroth, first, second or third order) or as an
        integer specifying the order of the spline interpolator to use.
        Default is 'linear'.
    window_length: positive odd integer
        The length of the Savitzky-Golay filter window (i.e., the number of coefficients).
        Default window_length=0, i.e. no smoothing.
    polyorder: int
        The order of the polynomial used to fit the samples.
        polyorder must be less than window_length.
    mode: 'mirror', 'constant', 'nearest', 'wrap' or 'interp'.
        Determines the type of extension to use for the padded signal to
        which the filter is applied.  When mode is 'constant', the padding
        value is given by cval.
        When the 'interp' mode is selected (the default), no extension
        is used.  Instead, a degree polyorder polynomial is fit to the
        last window_length values of the edges, and this polynomial is
        used to evaluate the last window_length // 2 output values.
    cval: scalar, optional
        Value to fill past the edges of the input if mode is 'constant'.
        Default is 0.0.

    Returns
    -------
    result: 3 x m array
        Interpolated n-vector(s) [no unit] decomposed in E.

    Notes
    -----
    The result for spherical Earth is returned.

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> import numpy as np
    >>> import nvector as nv
    >>> lat, lon = nv.rad(np.arange(0, 10)), np.sin(nv.rad(np.linspace(-90, 70, 10)))
    >>> t = np.arange(len(lat))
    >>> t_i = np.linspace(0, t[-1], 100)
    >>> nvectors = nv.lat_lon2n_E(lat, lon)
    >>> nvectors_i = nv.interp_nvectors(t_i, t, nvectors, kind='cubic')
    >>> lati, loni = nv.deg(*nv.n_E2lat_lon(nvectors_i))
    >>> h = plt.plot(nv.deg(lon), nv.deg(lat), 'o', loni, lati, '-')
    >>> plt.show()  # doctest: +SKIP
    >>> plt.close()

    Interpolate noisy data
    >>> n = 50
    >>> lat = nv.rad(np.linspace(0, 9, n));
    >>> lon = np.sin(nv.rad(np.linspace(-90, 70, n))) + 0.05* np.random.randn(n)
    >>> t = np.arange(len(lat))
    >>> t_i = np.linspace(0, t[-1], 100)
    >>> nvectors = nv.lat_lon2n_E(lat, lon)
    >>> nvectors_i = nv.interp_nvectors(t_i, t, nvectors, 'cubic', 31)
    >>> [lati, loni] = nv.n_E2lat_lon(nvectors_i)
    >>> h = plt.plot(nv.deg(lon), nv.deg(lat), 'o', nv.deg(loni), nv.deg(lati), '-')
    >>> plt.show()  # doctest: +SKIP
    >>> plt.close()


    """
    normal_i = _interp_vectors(t_i, t, nvectors, kind, window_length, polyorder, mode, cval)

    return unit(normal_i, norm_zero_vector=np.nan)


def interpolate(path, ti):
    """
    Returns the interpolated point along the path

    Parameters
    ----------
    path: tuple of n-vectors (positionA, positionB)

    ti: real scalar
        interpolation time assuming position A and B is at t0=0 and t1=1,
        respectively.

    Returns
    -------
    point: Nvector
        point of interpolation along path

    Notes
    -----
    The result for spherical Earth is returned.

    """

    n_EB_E_t0, n_EB_E_t1 = path
    n_EB_E_ti = unit(n_EB_E_t0 + ti * (n_EB_E_t1 - n_EB_E_t0),
                     norm_zero_vector=np.nan)
    return n_EB_E_ti


@use_docstring(_examples.get_examples_no_header([9], oo_solution=False))
def intersect(path_a, path_b):
    """
    Returns the intersection(s) between the great circles of the two paths

    Parameters
    ----------
    path_a, path_b: tuples of two n-vectors
        defining path A and path B, respectively.
        Path A and B has shape 2 x 3 x n and 2 x 3 x m, respectively.

    Returns
    -------
    n_EC_E : array of shape 3 x max(n, m)
        n-vector(s) [no unit] of position C decomposed in E.
        point(s) of intersection between paths.

    Notes
    -----
    The result for spherical Earth is returned.
    The shape of the output `n_EC_E` is the broadcasted shapes of `path_a` and `path_b`.

    Examples
    --------
    {super}

    """
    n_EA1_E, n_EA2_E = path_a
    n_EB1_E, n_EB2_E = path_b
    # Find the intersection between the two paths, n_EC_E:
    n_EC_E_tmp = unit(cross(cross(n_EA1_E, n_EA2_E, axis=0),
                            cross(n_EB1_E, n_EB2_E, axis=0), axis=0),
                      norm_zero_vector=np.nan)

    # n_EC_E_tmp is one of two solutions, the other is -n_EC_E_tmp. Select
    # the one that is closet to n_EA1_E, by selecting sign from the dot
    # product between n_EC_E_tmp and n_EA1_E:
    n_EC_E = np.sign(dot(n_EC_E_tmp.T, n_EA1_E)) * n_EC_E_tmp
    if np.any(np.isnan(n_EC_E)):
        warnings.warn('Paths are Equal. Intersection point undefined. '
                      'NaN returned.')
    return n_EC_E


def _check_window_length(window_length, data):
    """Make sure window length is odd and shorter than the length of the data"""
    n = len(data)
    window_length = window_length + (window_length + 1) % 2  # make sure it is an odd integer
    if window_length >= n:
        new_length = max(n - 1 - n % 2, 1)
        warnings.warn('Window length must be smaller than {}, but got {}!'
                      ' Truncating to {}!'.format(n, window_length, new_length))
        window_length = new_length
    return window_length


def course_over_ground(nvectors, a=6378137, f=1.0 / 298.257223563, R_Ee=None, **options):
    """Returns course over ground in radians from nvector positions

    Parameters
    ----------
    nvectors:  3 x n array
        Positions of vehicle given as n-vectors [no unit] decomposed in E.
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.
    window_length: positive odd integer {0}
        The length of the Savitzky-Golay filter window (i.e., the number of coefficients).
        Default window_length=0, i.e. no smoothing.
    polyorder: int {2}
        The order of the polynomial used to fit the samples.
        polyorder must be less than window_length.
    mode: 'mirror', 'constant', {'nearest'}, 'wrap' or 'interp'.
        Determines the type of extension to use for the padded signal to
        which the filter is applied.  When mode is 'constant', the padding
        value is given by cval. When the 'nearest' mode is selected (the default)
        the extension contains the nearest input value.
        When the 'interp' mode is selected, no extension
        is used.  Instead, a degree polyorder polynomial is fit to the
        last window_length values of the edges, and this polynomial is
        used to evaluate the last window_length // 2 output values.
    cval: scalar, optional
        Value to fill past the edges of the input if mode is 'constant'.
        Default is 0.0.

    Returns
    -------
    cog: array of length n
        angle in radians clockwise from True North to the direction towards
        which the vehicle travels.

    Notes
    -----
    Please be aware that this method requires the vehicle positions to be very smooth!
    If they are not you should probably smooth it by a window_length corresponding
    to a few seconds or so.

    See https://www.navlab.net/Publications/The_Seven_Ways_to_Find_Heading.pdf
    for an overview of methods to find accurate headings.

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> import nvector as nv
    >>> lats = nv.rad(59.381509, 59.387647)
    >>> lons = nv.rad(10.496590, 10.494713)
    >>> nvec = nv.lat_lon2n_E(lats, lons)
    >>> COG_rad = nv.course_over_ground(nvec)
    >>> dx, dy = np.sin(COG_rad[0]), np.cos(COG_rad[0])
    >>> COG = nv.deg(COG_rad)
    >>> p_AB_N = n_EA_E_and_n_EB_E2p_AB_N(nvec[:, :1], nvec[:, 1:]).ravel()
    >>> ax = plt.figure().gca()
    >>> _ = ax.plot(0, 0, 'bo', label='A')
    >>> _ = ax.arrow(0,0, dx*300, dy*300, head_width=20, label='COG')
    >>> _ = ax.plot(p_AB_N[1], p_AB_N[0], 'go', label='B')
    >>> _ = ax.set_title('COG={} degrees'.format(COG))
    >>> _ = ax.set_xlabel('East [m]')
    >>> _ = ax.set_ylabel('North [m]')
    >>> _ = ax.set_xlim(-500, 200)
    >>> _ = ax.set_aspect('equal', adjustable='box')
    >>> _ = ax.legend()
    >>> plt.show()  # doctest: +SKIP
    >>> plt.close()

    See also
    --------
    n_EA_E_and_n_EB_E2azimuth
    """
    nvectors = np.atleast_2d(nvectors)
    if nvectors.shape[1] < 2:
        return np.nan
    window_length = options.pop('window_length', 0)
    if window_length > 0:
        window_length = _check_window_length(window_length, nvectors[0])
        polyorder = options.pop('polyorder', 2)
        mode = options.pop('mode', 'nearest')
        if mode not in {'nearest', 'interp'}:
            warnings.warn('Using {} is not a recommended mode for filtering headings data!'
                          ' Use "interp" or "nearest" mode instead!'.format(mode))
        cval = options.pop('cval', 0.0)
        normal = savgol_filter(nvectors, window_length, polyorder, axis=1, mode=mode, cval=cval)
    else:
        normal = nvectors
    n_vecs = np.hstack((normal[:, :1], unit(normal[:, :-1] + normal[:, 1:]), normal[:, -1:]))

    return n_EA_E_and_n_EB_E2azimuth(n_vecs[:, :-1], n_vecs[:, 1:], a=a, f=f, R_Ee=R_Ee)


def great_circle_normal(n_EA_E, n_EB_E):
    """
    Returns the unit normal(s) to the great circle(s)

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x k and 3 x m arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.

    Returns
    -------
    normal : 3 x max(k, m) array
        Unit normal(s)

    Notes
    -----
    The shape of the output `normal` is the broadcasted shapes of `n_EA_E`and `n_EB_E`.
    """
    return unit(cross(n_EA_E, n_EB_E, axis=0), norm_zero_vector=np.nan)


def _euclidean_cross_track_distance(sin_theta, radius=1):
    return sin_theta * radius


def _great_circle_cross_track_distance(sin_theta, radius=1):
    return np.arcsin(sin_theta) * radius
    # ill conditioned for small angles:
    # return (np.arccos(-sin_theta) - np.pi / 2) * radius


@use_docstring(_examples.get_examples_no_header([10], oo_solution=False))
def cross_track_distance(path, n_EB_E, method='greatcircle', radius=6371009.0):
    """
    Returns cross track distance between path A and position B.

    Parameters
    ----------
    path: tuple of two n-vectors of shape 3 x k and 3 x m
        Two n-vectors of positions defining path A, decomposed in E.
    n_EB_E:  3 x n array
        n-vector(s) of position B to measure the cross track distance to.
    method: string
        defining distance calculated. Options are: 'greatcircle' or 'euclidean'
    radius: real scalar
        radius of sphere [m]. (default 6371009.0)

    Returns
    -------
    distance : array of length max(k, m, n)
        cross track distance(s)

    Notes
    -----
    The result for spherical Earth is returned.
    The shape of the output `distance` is the broadcasted shapes of `n_EB_E` and
    the n-vectors defining path A.

    Examples
    --------
    {super}

    See also
    --------
    great_circle_normal, closest_point_on_great_circle, on_great_circle, on_great_circle_path
    """
    c_E = great_circle_normal(path[0], path[1])
    sin_theta = -np.sum(c_E * n_EB_E, axis=0)
    if method[0].lower() == 'e':
        return _euclidean_cross_track_distance(sin_theta, radius)
    return _great_circle_cross_track_distance(sin_theta, radius)


@use_docstring(_examples.get_examples_no_header([10], oo_solution=False))
def on_great_circle(path, n_EB_E, radius=6371009.0, atol=1e-8):
    """
    Returns True if position B is on great circle through path A.

    Parameters
    ----------
    path: tuple of two n-vectors of shapes 3 x k and 3 x m.
        Two n-vectors of positions defining path A, decomposed in E.
    n_EB_E:  3 x n array
        n-vector(s) of position B to check to.
    radius: real scalar
        radius of sphere. (default 6371009.0)
    atol: real scalar
        The absolute tolerance parameter (See notes).

    Returns
    -------
    on : max(k, m, n) bool array
        True if position B is on great circle through path A.

    Notes
    -----
    The default value of `atol` is not zero, and is used to determine what
    small values should be considered close to zero. The default value is
    appropriate for expected values of order unity. However, `atol` should
    be carefully selected for the use case at hand. Typically the value
    should be set to the accepted error tolerance. For GPS data the error
    ranges from 0.01 m to 15 m.
    The shape of the output `on` is the broadcasted size of `n_EB_E` and `path`.

    Examples
    --------
    {super}

    See also
    --------
    cross_track_distance
    """
    distance = np.abs(cross_track_distance(path, n_EB_E, radius=radius))
    return distance <= atol


@use_docstring(_examples.get_examples_no_header([10], oo_solution=False))
def on_great_circle_path(path, n_EB_E, radius=6371009.0, atol=1e-8):
    """
    Returns True if position B is on great circle and between endpoints of path A.

    Parameters
    ----------
    path: tuple of two n-vectors of shapes 3 x k and 3 x m.
        Two n-vectors of positions defining path A, decomposed in E.
    n_EB_E:  3 x n array
        n-vector(s) of position B to measure the cross track distance to.
    radius: real scalar
        radius of sphere. (default 6371009.0)
    atol: real scalars
        The absolute tolerance parameter (See notes).

    Returns
    -------
    on : max(k, m, n) bool array
        True if position B is on great circle and between endpoints of path A.

    Notes
    -----
    The default value of `atol` is not zero, and is used to determine what
    small values should be considered close to zero. The default value is
    appropriate for expected values of order unity. However, `atol` should
    be carefully selected for the use case at hand. Typically the value
    should be set to the accepted error tolerance. For GPS data the error
    ranges from 0.01 m to 15 m.
    The shape of the output `on` is the broadcasted shapes of `n_EB_E` and `path`.

    Examples
    --------
    {super}

    See also
    --------
    cross_track_distance, on_great_circle
    """
    n_EB_E, n_EA1_E, n_EA2_E = np.atleast_2d(n_EB_E, *path)
    scale = norm(n_EA2_E - n_EA1_E, axis=0)
    ti1 = norm(n_EB_E - n_EA1_E, axis=0) / scale
    ti2 = norm(n_EB_E - n_EA2_E, axis=0) / scale
    return (ti1 <= 1) & (ti2 <= 1) & on_great_circle(path, n_EB_E, radius, atol=atol)


@use_docstring(_examples.get_examples_no_header([10], oo_solution=False))
def closest_point_on_great_circle(path, n_EB_E):
    """
    Returns closest point C on great circle path A to position B.

    Parameters
    ----------
    path: tuple of two n-vectors of shape 3 x k  and 3 x m
        Two n-vectors of positions defining path A, decomposed in E.
    n_EB_E:  3 x n array
        n-vector(s) of position B to find the closest point to.

    Returns
    -------
    n_EC_E:  3 x max(k, m, n) array
        n-vector(s) of closest position C on great circle path A

    Notes
    -----
    The shape of the output `n_EC_E` is the broadcasted shapes of `n_EB_E` and
    the n-vectors defining path A.

    Examples
    --------
    {super}

    See also
    --------
    cross_track_distance, great_circle_normal

    """
    n_EA1_E, n_EA2_E = path
    c_E = great_circle_normal(n_EA1_E, n_EA2_E)

    c2 = cross(n_EB_E, c_E, axis=0)
    n_EC_E = unit(cross(c_E, c2, axis=0))
    return n_EC_E * np.sign(np.sum(n_EC_E * n_EB_E, axis=0, keepdims=True))


def _azimuth_sphere(n_EA_E, n_EB_E, R_Ee=None):
    """Returns azimuths from A to B and B to A, relative to North on a sphere


    See also
    https://en.wikipedia.org/wiki/Azimuth
    """
    lat1, lon1 = n_E2lat_lon(n_EA_E, R_Ee)
    lat2, lon2 = n_E2lat_lon(n_EB_E, R_Ee)

    w = lon2 - lon1
    cos_b1, sin_b1 = cos(lat1), sin(lat1)
    cos_b2, sin_b2 = cos(lat2), sin(lat2)
    cos_w, sin_w = cos(w), sin(w)

    cos_az1 = cos_b1 * sin_b2 - sin_b1 * cos_b2 * cos_w
    sin_az1 = cos_b2 * sin_w

    cos_az2 = cos_b2 * sin_b1 - sin_b2 * cos_b1 * cos_w
    sin_az2 = -cos_b1 * sin_w
    return np.arctan2(sin_az1, cos_az1), np.arctan2(sin_az2, cos_az2)


def great_circle_distance_rad(n_EA_E, n_EB_E, R_Ee=None):
    """
    Returns great circle distance in radians between positions A and B on a sphere

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x k and 3 x m arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.

    Returns
    -------
    distance_rad : array of length max(k, m)
        Great circle distance(s) in radians

    Notes
    -----
    The result for spherical Earth is returned.
    The shape of the output `distance_rad` is the broadcasted shapes of `n_EA_E`and `n_EB_E`.
    Formulae is given by equation (16) in Gade (2010) and is well
    conditioned for all angles.
    See also: https://en.wikipedia.org/wiki/Great-circle_distance.

    See also
    --------
    great_circle_distance
    """
    if R_Ee is None:
        R_Ee = E_rotation()
    n_EA_E, n_EB_E = np.atleast_2d(n_EA_E, n_EB_E)

    sin_theta = norm(np.cross(n_EA_E, n_EB_E, axis=0), axis=0)
    cos_theta = np.sum(n_EA_E * n_EB_E, axis=0)

    # Alternatively:
    # sin_phi = norm(n_EA_E-n_EB_E, axis=0)/2  # phi = theta/2
    # cos_phi = norm(n_EA_E+n_EB_E, axis=0)/2
    # theta = 2 * np.arctan2(sin_phi, cos_phi)

    # ill conditioned for small angles:
    # distance_rad_version1 = arccos(dot(n_EA_E,n_EB_E))

    # ill-conditioned for angles near pi/2 (and not valid above pi/2)
    # distance_rad_version2 = arcsin(norm(cross(n_EA_E,n_EB_E)))

    return np.arctan2(sin_theta, cos_theta)


@use_docstring(_examples.get_examples_no_header([5], oo_solution=False))
def great_circle_distance(n_EA_E, n_EB_E, radius=6371009.0):
    """
    Returns great circle distance between positions A and B on a sphere

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x k and 3 x m arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.
    radius: real scalar
        radius of sphere [m]. (default 6371009.0)

    Returns
    -------
    distance : array of length max(k, m)
        Great circle distance(s) in meters

    Notes
    -----
    The result for spherical Earth is returned.
    The shape of the output `distance` is the broadcasted shapes of `n_EA_E` and `n_EB_E`.
    Formulae is given by equation (16) in Gade (2010) and is well
    conditioned for all angles.
    See also: https://en.wikipedia.org/wiki/Great-circle_distance.

    Examples
    --------
    {super}

    See also
    --------
    great_circle_distance_rad
    """
    return great_circle_distance_rad(n_EA_E, n_EB_E) * radius


def geodesic_reckon(n_EA_E, distance, azimuth, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns position B computed from position A, distance and azimuth.

    Parameters
    ----------


    Returns position B given surface distance between positions A and B on an ellipsoid.

    Parameters
    ----------
    n_EA_E:  3 x m arrays
        n-vector(s) [no unit] of position A, decomposed in E.
    distance: real scalar or vector of length n.
        ellipsoidal distance [m] between position A and B.
    azimuth: real scalar or vector of length n.
        azimuth [rad or deg] of line at position A.
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    n_EB_E:  3 x max(m,n) arrays
        n-vector(s) [no unit] of position B, decomposed in E.
    azimuth_b: real scalars or vectors of length max(m,n).
        azimuth [rad or deg] of line at position B.

    Examples
    --------
    >>> import numpy as np
    >>> import nvector as nv
    """

    lat1, lon1 = n_E2lat_lon(n_EA_E, R_Ee)
    lat2, lon2, alpha2 = _geodesic_reckon(lat1, lon1, distance, azimuth, a, f)
#     n1_e = change_axes_to_E(n_EA_E, R_Ee)
#     sin_lat1 = n1_e[0, :]
#     cos_lat1 = sqrt(n1_e[1, :]**2 + n1_e[2, :]**2)

    n_EB_E = lat_lon2n_E(lat2, lon2, R_Ee)
    return n_EB_E, alpha2


def geodesic_distance(n_EA_E, n_EB_E, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns surface distance between positions A and B on an ellipsoid.

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x m  and 3 x n arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    distance:  real scalars or vectors of length max(m,n).
        Surface distance [m] from A to B on the ellipsoid
    azimuth_a, azimuth_b: real scalars or vectors of length max(m,n).
        direction [rad or deg] of line at position a and b relative to
        North, respectively.

    Examples
    --------
    >>> import numpy as np
    >>> import nvector as nv
    >>> n_EA_E = nv.lat_lon2n_E(0,0)
    >>> n_EB_E = nv.lat_lon2n_E(*nv.rad(0.5, 179.5))
    >>> s12, az1, az2 = nv.geodesic_distance(n_EA_E, n_EB_E)
    >>> np.allclose(s12, 19936288.578965)
    True
    >>> np.allclose(nv.deg(az1, az2), (25.67187286829021, 154.32708546994326))
    True

    See also
    --------
    euclidean_distance, great_circle_distance, n_EB_E2p_EB_E

    """
    # From C.F.F. Karney (2011) "Algorithms for geodesics":
    # See also https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid
    lat1, lon1 = n_E2lat_lon(n_EA_E, R_Ee)
    lat2, lon2 = n_E2lat_lon(n_EB_E, R_Ee)
    s12, az1, az2 = _geodesic_distance(lat1, lon1, lat2, lon2, a, f)
    return s12, az1, az2


@use_docstring(_examples.get_examples_no_header([5], oo_solution=False))
def euclidean_distance(n_EA_E, n_EB_E, radius=6371009.0):
    """
    Returns Euclidean distance between positions A and B on a sphere

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x k and 3 x m arrays
        n-vector(s) [no unit] of position A and B, decomposed in E.
    radius: real scalar
        radius of sphere [m]. (default 6371009.0)

    Returns
    -------
    distance : array of length max(k, m)
        Euclidean distance(s)

    Notes
    -----
    The shape of the output `distance` is the broadcasted shapes of `n_EB_E` and `n_EB_E`.

    Examples
    --------
    {super}
    """
    n_EB_E, n_EA_E = np.atleast_2d(n_EB_E, n_EA_E)
    d_AB = norm(n_EB_E - n_EA_E, axis=0).ravel() * radius
    return d_AB


def n_EA_E_and_n_EB_E2azimuth(n_EA_E, n_EB_E, a=6378137, f=1.0 / 298.257223563, R_Ee=None):
    """
    Returns azimuth from A to B, relative to North:

    Parameters
    ----------
    n_EA_E, n_EB_E:  3 x m  and 3 x n arrays
        n-vector(s) [no unit] of position A and B, respectively, decomposed in E.
    a: real scalar, default WGS-84 ellipsoid.
        Semi-major axis of the Earth ellipsoid given in [m].
    f: real scalar, default WGS-84 ellipsoid.
        Flattening [no unit] of the Earth ellipsoid. If f==0 then spherical
        Earth with radius a is used in stead of WGS-84.
    R_Ee : 3 x 3 array
        rotation matrix defining the axes of the coordinate frame E.

    Returns
    -------
    azimuth: max(m, n) array
        Angle [rad] the line makes with a meridian, taken clockwise from north.

    Notes
    -----
    The shape of the output `azimuth` is the broadcasted shapes of `n_EA_E` and `n_EB_E`.

    See also
    --------
    great_circle_distance_rad, n_EA_E_distance_and_azimuth2n_EB_E, course_over_ground
    """
    if R_Ee is None:
        R_Ee = E_rotation()

    #  Find p_AB_N (delta decomposed in N).
    p_AB_N = n_EA_E_and_n_EB_E2p_AB_N(n_EA_E, n_EB_E, z_EA=0, z_EB=0, a=a, f=f, R_Ee=R_Ee)

    # Find the direction (azimuth) to B, relative to north:

    return arctan2(p_AB_N[1], p_AB_N[0])


@use_docstring(_examples.get_examples_no_header([8], oo_solution=False))
def n_EA_E_distance_and_azimuth2n_EB_E(n_EA_E, distance_rad, azimuth, R_Ee=None):
    """
    Returns position B from azimuth and distance from position A

    Parameters
    ----------
    n_EA_E:  3 x k array
        n-vector(s) [no unit] of position A decomposed in E.
    distance_rad: m array
        great circle distance [rad] from position A to B
    azimuth: n array
        Angle [rad] the line makes with a meridian, taken clockwise from north.

    Returns
    -------
    n_EB_E:  3 x max(k,m,n) array
        n-vector(s) [no unit] of position B decomposed in E.

    Notes
    -----
    The result for spherical Earth is returned.
    The shape of the output `n_EB_E` is the broadcasted shapes of `n_EA_E`,
    `distance_rad` and `azimuth.

    Examples
    --------
    {super}

    See also
    --------
    n_EA_E_and_n_EB_E2azimuth, great_circle_distance_rad
    """

    if R_Ee is None:
        R_Ee = E_rotation()
    n_EA_E, distance_rad, azimuth = np.atleast_1d(n_EA_E, distance_rad, azimuth)
    # Step1: Find unit vectors for north and east:
    k_east_E = unit(cross(dot(R_Ee.T, [[1], [0], [0]]), n_EA_E, axis=0))
    k_north_E = cross(n_EA_E, k_east_E, axis=0)

    # Step2: Find the initial direction vector d_E:
    d_E = k_north_E * cos(azimuth) + k_east_E * sin(azimuth)

    # Step3: Find n_EB_E:
    n_EB_E = n_EA_E * cos(distance_rad) + d_E * sin(distance_rad)

    return n_EB_E


@use_docstring(_examples.get_examples_no_header([7], oo_solution=False))
def mean_horizontal_position(n_EB_E):
    """
    Returns the n-vector of the horizontal mean position.

    Parameters
    ----------
    n_EB_E:  3 x n array
        n-vectors [no unit] of positions Bi, decomposed in E.

    Returns
    -------
    p_EM_E:  3 x 1 array
        n-vector [no unit] of the mean positions of all Bi, decomposed in E.

    Notes
    -----
    The result for spherical Earth is returned.

    Examples
    --------
    {super}

    """
    n_EM_E = unit(np.sum(n_EB_E, axis=1).reshape((3, 1)))
    return n_EM_E


_odict = globals()
__doc__ = (__doc__  # @ReservedAssignment
           + _make_summary(dict((n, _odict[n]) for n in __all__))
           + 'License\n-------\n'
           + _license.__doc__)


if __name__ == "__main__":
    test_docstrings(__file__)