[Git][debian-gis-team/pymap3d][upstream] New upstream version 2.7.2

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Wed Oct 20 14:03:33 BST 2021



Antonio Valentino pushed to branch upstream at Debian GIS Project / pymap3d


Commits:
6e091f6a by Antonio Valentino at 2021-10-20T12:14:38+00:00
New upstream version 2.7.2
- - - - -


30 changed files:

- .github/workflows/ci.yml
- .github/workflows/ci_stdlib_only.yml
- − .github/workflows/codeql-analysis.yml
- Examples/angle_distance.py
- + Examples/has_map_toolbox.m
- + Examples/lox_stability.py
- Examples/vdist_poi.py
- Examples/vdist_stability.py
- README.md
- pyproject.toml
- setup.cfg
- src/pymap3d/aer.py
- src/pymap3d/ecef.py
- src/pymap3d/eci.py
- src/pymap3d/ellipsoid.py
- src/pymap3d/enu.py
- src/pymap3d/latitude.py
- src/pymap3d/los.py
- src/pymap3d/lox.py
- src/pymap3d/ned.py
- src/pymap3d/rcurve.py
- src/pymap3d/rsphere.py
- src/pymap3d/sidereal.py
- src/pymap3d/tests/test_latitude.py
- src/pymap3d/tests/test_rcurve.py
- src/pymap3d/tests/test_rhumb.py
- src/pymap3d/timeconv.py
- src/pymap3d/utils.py
- src/pymap3d/vincenty.py
- src/pymap3d/vreckon/__main__.py


Changes:

=====================================
.github/workflows/ci.yml
=====================================
@@ -5,7 +5,9 @@ on:
     paths:
     - "**.py"
     - .github/workflows/ci.yml
-
+    pull-request:
+    - "**.py"
+    - .github/workflows/ci_stdlib_only.yml
 
 jobs:
 


=====================================
.github/workflows/ci_stdlib_only.yml
=====================================
@@ -5,17 +5,25 @@ on:
     paths:
     - "**.py"
     - .github/workflows/ci_stdlib_only.yml
-
+    pull-request:
+    - "**.py"
+    - .github/workflows/ci_stdlib_only.yml
 
 jobs:
 
   stdlib_only:
     runs-on: ubuntu-latest
+
+    name: Python ${{ matrix.python-version }}
+    strategy:
+      matrix:
+        python-version: [ '3.7', '3.10' ]
+
     steps:
     - uses: actions/checkout at v2
     - uses: actions/setup-python at v2
       with:
-        python-version: 3.7
+        python-version: ${{ matrix.python-version }}
 
     - run: pip install .[tests]
 


=====================================
.github/workflows/codeql-analysis.yml deleted
=====================================
@@ -1,31 +0,0 @@
-name: "CodeQL"
-
-on:
-  schedule:
-    - cron: '0 10 * * 6'
-
-jobs:
-  analyse:
-    name: Analyse
-    runs-on: ubuntu-latest
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout at v2
-      with:
-        # We must fetch at least the immediate parents so that if this is
-        # a pull request then we can checkout the head.
-        fetch-depth: 2
-
-    # If this run was triggered by a pull request event, then checkout
-    # the head of the pull request instead of the merge commit.
-    - run: git checkout HEAD^2
-      if: ${{ github.event_name == 'pull_request' }}
-
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init at v1
-      with:
-         languages: python
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze at v1


=====================================
Examples/angle_distance.py
=====================================
@@ -15,7 +15,7 @@ def main():
     dist_deg = anglesep_meeus(a.r0, a.d0, a.r1, a.d1)
     dist_deg_astropy = anglesep(a.r0, a.d0, a.r1, a.d1)
 
-    print("{:.6f} deg sep".format(dist_deg))
+    print(f"{dist_deg:.6f} deg sep")
 
     assert dist_deg == approx(dist_deg_astropy)
 


=====================================
Examples/has_map_toolbox.m
=====================================
@@ -0,0 +1,3 @@
+function has_map = has_map_toolbox()
+  has_map = ~isempty(ver("map"));
+end


=====================================
Examples/lox_stability.py
=====================================
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+import logging
+from pathlib import Path
+from math import isclose
+
+from pymap3d.lox import loxodrome_direct
+
+import matlab.engine
+
+cwd = Path(__file__).parent
+eng = None  # don't start Matlab engine over and over when script is interactive
+
+if eng is None:
+    eng = matlab.engine.start_matlab("-nojvm")
+    eng.addpath(eng.genpath(str(cwd)), nargout=0)
+
+if not eng.has_map_toolbox():
+    raise EnvironmentError("Matlab does not have Mapping Toolbox")
+
+
+def matlab_func(lat1: float, lon1: float, rng: float, az: float) -> tuple[float, float]:
+    """Using Matlab Engine to do same thing as Pymap3d"""
+    return eng.reckon("rh", lat1, lon1, rng, az, eng.wgs84Ellipsoid(), nargout=2)  # type: ignore
+
+
+clat, clon, rng = 35.0, 140.0, 50000.0  # arbitrary
+
+for i in range(20):
+    for azi in (90 + 10.0 ** (-i), -90 + 10.0 ** (-i), 270 + 10.0 ** (-i), -270 + 10.0 ** (-i)):
+        lat, lon = loxodrome_direct(clat, clon, rng, azi)
+
+        lat_matlab, lon_matlab = matlab_func(clat, clon, rng, azi)
+        rstr = f"azimuth: {azi} lat,lon: Python: {lat}, {lon}  Matlab: {lat_matlab}, {lon_matlab}"
+        if not (
+            isclose(lat_matlab, lat, rel_tol=0.005) and isclose(lon_matlab, lon, rel_tol=0.001)
+        ):
+            logging.error(rstr)


=====================================
Examples/vdist_poi.py
=====================================
@@ -32,13 +32,13 @@ def get_place_coords(
     keyfn = Path(keyfn).expanduser()
     key = keyfn.read_text()
 
-    stub = URL + "location={},{}".format(latitude, longitude)
+    stub = URL + f"location={latitude},{longitude}"
 
-    stub += "&radius={}".format(search_radius_km * 1000)
+    stub += f"&radius={search_radius_km * 1000}"
 
-    stub += "&types={}".format(place_type)
+    stub += f"&types={place_type}"
 
-    stub += "&key={}".format(key)
+    stub += f"&key={key}"
 
     r = requests.get(stub)
     r.raise_for_status()


=====================================
Examples/vdist_stability.py
=====================================
@@ -1,24 +1,26 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
+from __future__ import annotations
 from pymap3d.vincenty import vdist
 import sys
+from pathlib import Path
 from math import isclose, nan
-import typing
 
-eng = None  # don't start engine over and over when script is interactive
-try:
-    import matlab.engine
+import matlab.engine
 
-    if eng is None:
-        eng = matlab.engine.start_matlab("-nojvm")
-except ImportError as exc:
-    print(exc, file=sys.stderr)
-    eng = None
+cwd = Path(__file__).parent
+eng = None  # don't start Matlab engine over and over when script is interactive
 
+if eng is None:
+    eng = matlab.engine.start_matlab("-nojvm")
+    eng.addpath(eng.genpath(str(cwd)), nargout=0)
 
-def matlab_func(lat1: float, lon1: float, lat2: float, lon2: float) -> typing.Tuple[float, float]:
+if not eng.has_map_toolbox():
+    raise EnvironmentError("Matlab does not have Mapping Toolbox")
+
+
+def matlab_func(lat1, lon1, lat2, lon2) -> tuple[float, float]:
     """Using Matlab Engine to do same thing as Pymap3d"""
-    ell = eng.wgs84Ellipsoid()
-    return eng.distance(lat1, lon1, lat2, lon2, ell, nargout=2)
+    return eng.distance(lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), nargout=2)  # type: ignore
 
 
 dlast, alast = nan, nan


=====================================
README.md
=====================================
@@ -5,8 +5,6 @@
 [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/geospace-code/pymap3d.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/geospace-code/pymap3d/context:python)
 ![Actions Status](https://github.com/geospace-code/pymap3d/workflows/ci/badge.svg)
 ![Actions Status](https://github.com/geospace-code/pymap3d/workflows/ci_stdlib_only/badge.svg)
-![CodeQL](https://github.com/geospace-code/pymap3d/workflows/CodeQL/badge.svg)
-[![codecov](https://codecov.io/gh/geospace-code/pymap3d/branch/master/graph/badge.svg)](https://codecov.io/gh/geospace-code/pymap3d)
 [![image](https://img.shields.io/pypi/pyversions/pymap3d.svg)](https://pypi.python.org/pypi/pymap3d)
 [![PyPi Download stats](http://pepy.tech/badge/pymap3d)](http://pepy.tech/project/pymap3d)
 
@@ -16,12 +14,12 @@ PyMap3D is intended for non-interactive use on massively parallel (HPC) and embe
 
 [API docs](https://geospace-code.github.io/pymap3d/)
 
-Thanks to our [contributors](./contributors.md).
+Thanks to our [contributors](./.github/contributors.md).
 
 ## Similar toolboxes in other code languages
 
 * [Matlab, GNU Octave](https://github.com/geospace-code/matmap3d)
-* [Fortran](https://github.com/geospace-code/maptran)
+* [Fortran](https://github.com/geospace-code/maptran3d)
 * [Rust](https://github.com/gberrante/map_3d)
 
 ## Prerequisites
@@ -46,7 +44,7 @@ pip install -e pymap3d
 One can verify Python functionality after installation by:
 
 ```sh
-pytest pymap3d -r a -v
+pytest pymap3d
 ```
 
 ## Usage
@@ -63,7 +61,7 @@ az,el,range = pm.geodetic2aer(lat, lon, alt, observer_lat, observer_lon, 0)
 ```
 
 [Python](https://www.python.org/dev/peps/pep-0448/)
-[argument unpacking](https://docs.python.org/3.6/tutorial/controlflow.html#unpacking-argument-lists)
+[argument unpacking](https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists)
 can be used for compact function arguments with scalars or arbitrarily
 shaped N-D arrays:
 
@@ -76,7 +74,7 @@ lla = pm.aer2geodetic(*aer,*obslla)
 
 where tuple `lla` is comprised of scalar or N-D arrays `(lat,lon,alt)`.
 
-Example scripts are in the [examples](./examples) directory.
+Example scripts are in the [examples](./Examples) directory.
 
 ### Functions
 


=====================================
pyproject.toml
=====================================
@@ -39,6 +39,3 @@ full = [
 
 [tool.black]
 line-length = 100
-
-[tool.pytest.ini_options]
-addopts = "-ra"


=====================================
setup.cfg
=====================================
@@ -1,6 +1,6 @@
 [metadata]
 name = pymap3d
-version = 2.7.0
+version = 2.7.2
 author = Michael Hirsch, Ph.D.
 author_email = scivision at users.noreply.github.com
 description = pure Python (no prereqs) coordinate conversions, following convention of several popular Matlab routines.
@@ -38,6 +38,8 @@ lint =
   flake8-builtins
   flake8-blind-except
   mypy >= 0.800
+  types-python-dateutil
+  types-requests
 full =
   python-dateutil
   numpy >= 1.10.0


=====================================
src/pymap3d/aer.py
=====================================
@@ -10,11 +10,11 @@ from .ellipsoid import Ellipsoid
 
 try:
     from .eci import eci2ecef, ecef2eci
-    from numpy import ndarray
 except ImportError:
-    eci2ecef = ecef2eci = None
-    ndarray = typing.Any  # type: ignore
+    eci2ecef = ecef2eci = None  # type: ignore
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
 
 __all__ = ["aer2ecef", "ecef2aer", "geodetic2aer", "aer2geodetic", "eci2aer", "aer2eci"]
 


=====================================
src/pymap3d/ecef.py
=====================================
@@ -3,24 +3,11 @@ from __future__ import annotations
 import typing
 
 try:
-    from numpy import (
-        ndarray,
-        radians,
-        sin,
-        cos,
-        tan,
-        arctan as atan,
-        hypot,
-        degrees,
-        arctan2 as atan2,
-        sqrt,
-        pi,
-    )
+    from numpy import radians, sin, cos, tan, arctan as atan, hypot, degrees, arctan2 as atan2, sqrt
 except ImportError:
-    from math import radians, sin, cos, tan, atan, hypot, degrees, atan2, sqrt, pi  # type: ignore
-
-    ndarray = typing.Any  # type: ignore
+    from math import radians, sin, cos, tan, atan, hypot, degrees, atan2, sqrt  # type: ignore
 
+from math import pi
 from datetime import datetime
 
 from .ellipsoid import Ellipsoid
@@ -29,10 +16,10 @@ from .utils import sanitize
 try:
     from .eci import eci2ecef, ecef2eci
 except ImportError:
-    eci2ecef = ecef2eci = None
+    eci2ecef = ecef2eci = None  # type: ignore
 
-# py < 3.6 compatible
-tau = 2 * pi
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
 
 
 __all__ = [


=====================================
src/pymap3d/eci.py
=====================================
@@ -2,8 +2,9 @@
 
 from __future__ import annotations
 
+import typing
 from datetime import datetime
-from numpy import ndarray, array, sin, cos, column_stack, empty, atleast_1d
+from numpy import array, sin, cos, column_stack, empty, atleast_1d
 
 try:
     from astropy.coordinates import GCRS, ITRS, EarthLocation, CartesianRepresentation
@@ -14,16 +15,14 @@ except ImportError:
 
 from .sidereal import greenwichsrt, juliandate
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["eci2ecef", "ecef2eci"]
 
 
 def eci2ecef(
-    x: float | ndarray,
-    y: float | ndarray,
-    z: float | ndarray,
-    time: datetime,
-    *,
-    use_astropy: bool = True
+    x: ndarray, y: ndarray, z: ndarray, time: datetime, *, use_astropy: bool = True
 ) -> tuple[ndarray, ndarray, ndarray]:
     """
     Observer => Point  ECI  =>  ECEF
@@ -81,12 +80,7 @@ def eci2ecef(
 
 
 def ecef2eci(
-    x: float | ndarray,
-    y: float | ndarray,
-    z: float | ndarray,
-    time: datetime,
-    *,
-    use_astropy: bool = True
+    x: ndarray, y: ndarray, z: ndarray, time: datetime, *, use_astropy: bool = True
 ) -> tuple[ndarray, ndarray, ndarray]:
     """
     Point => Point   ECEF => ECI


=====================================
src/pymap3d/ellipsoid.py
=====================================
@@ -22,6 +22,7 @@ class Ellipsoid:
         model : str
                 name of ellipsoid
         """
+
         if model == "wgs84":
             """https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84"""
             self.semimajor_axis = 6378137.0
@@ -62,9 +63,7 @@ class Ellipsoid:
             self.semiminor_axis = self.semimajor_axis
         else:
             raise NotImplementedError(
-                "{} model not implemented, let us know and we will add it (or make a pull request)".format(
-                    model
-                )
+                f"{model} model not implemented, let us know and we will add it (or make a pull request)"
             )
 
         self.flattening = (self.semimajor_axis - self.semiminor_axis) / self.semimajor_axis


=====================================
src/pymap3d/enu.py
=====================================
@@ -5,15 +5,16 @@ import typing
 from math import tau
 
 try:
-    from numpy import asarray, radians, sin, cos, hypot, arctan2 as atan2, degrees, ndarray
+    from numpy import asarray, radians, sin, cos, hypot, arctan2 as atan2, degrees
 except ImportError:
     from math import radians, sin, cos, hypot, atan2, degrees  # type: ignore
 
-    ndarray = typing.Any  # type: ignore
-
 from .ecef import geodetic2ecef, ecef2geodetic, enu2ecef, uvw2enu
 from .ellipsoid import Ellipsoid
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["enu2aer", "aer2enu", "enu2geodetic", "geodetic2enu"]
 
 
@@ -46,7 +47,7 @@ def enu2aer(
         slant range [meters]
     """
 
-    # 1 millimeter precision for singularity
+    # 1 millimeter precision for singularity stability
 
     try:
         e[abs(e) < 1e-3] = 0.0


=====================================
src/pymap3d/latitude.py
=====================================
@@ -4,20 +4,23 @@ from __future__ import annotations
 import typing
 
 from .ellipsoid import Ellipsoid
-from .utils import sanitize
+from .utils import sanitize, sign
 from . import rcurve
 
 try:
-    from numpy import radians, degrees, tan, sin, exp, pi, sqrt, inf, ndarray
+    from numpy import radians, degrees, tan, sin, cos, exp, pi, sqrt, inf
     from numpy import arctan as atan, arcsinh as asinh, arctanh as atanh  # noqa: A001
 
     use_numpy = True
 except ImportError:
-    from math import atan, radians, degrees, tan, sin, asinh, atanh, exp, pi, sqrt, inf  # type: ignore
+    from math import atan, radians, degrees, tan, sin, cos, asinh, atanh, exp, pi, sqrt, inf  # type: ignore
 
     use_numpy = False
-    ndarray = typing.Any  # type: ignore
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
+COS_EPS = 1e-9  # tolerance for angles near abs([90, 270])
 
 __all__ = [
     "geodetic2isometric",
@@ -85,8 +88,8 @@ def geoc2geod(
 
 
 def geodetic2geocentric(
-    geodetic_lat: float, alt_m: float, ell: Ellipsoid = None, deg: bool = True
-) -> float:
+    geodetic_lat: ndarray, alt_m: float, ell: Ellipsoid = None, deg: bool = True
+) -> ndarray:
     """
     convert geodetic latitude to geocentric latitude on spheroid surface
 
@@ -126,8 +129,8 @@ geod2geoc = geodetic2geocentric
 
 
 def geocentric2geodetic(
-    geocentric_lat: float, alt_m: float, ell: Ellipsoid = None, deg: bool = True
-) -> float:
+    geocentric_lat: ndarray, alt_m: float, ell: Ellipsoid = None, deg: bool = True
+) -> ndarray:
     """
     converts from geocentric latitude to geodetic latitude
 
@@ -163,7 +166,9 @@ def geocentric2geodetic(
     return degrees(geodetic_lat) if deg else geodetic_lat
 
 
-def geodetic2isometric(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
+def geodetic2isometric(
+    geodetic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True
+) -> float:
     """
     computes isometric latitude on an ellipsoid
 
@@ -205,19 +210,25 @@ def geodetic2isometric(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool =
     # isometric_lat = log(tan(a2) * (y ** (e / 2)))
     # isometric_lat = log(tan(a2)) + e/2 * log((1-e*sin(geodetic_lat)) / (1+e*sin(geodetic_lat)))
 
+    coslat = cos(geodetic_lat)
+    i = abs(coslat) <= COS_EPS
+
     try:
-        isometric_lat[abs(geodetic_lat - pi / 2) <= 1e-9] = inf
-        isometric_lat[abs(-geodetic_lat - pi / 2) <= 1e-9] = -inf
+        isometric_lat[i] = sign(geodetic_lat[i]) * inf  # type: ignore
     except TypeError:
-        if abs(geodetic_lat - pi / 2) <= 1e-9:
-            isometric_lat = inf
-        elif abs(-geodetic_lat - pi / 2) <= 1e-9:
-            isometric_lat = -inf
+        if i:
+            isometric_lat = sign(geodetic_lat) * inf
 
-    return degrees(isometric_lat) if deg else isometric_lat
+    if deg:
+        isometric_lat = degrees(isometric_lat)
+
+    try:
+        return isometric_lat.squeeze()[()]
+    except AttributeError:
+        return isometric_lat
 
 
-def isometric2geodetic(isometric_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def isometric2geodetic(isometric_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from isometric latitude to geodetic latitude
 
@@ -253,7 +264,7 @@ def isometric2geodetic(isometric_lat: float, ell: Ellipsoid = None, deg: bool =
     return degrees(geodetic_lat) if deg else geodetic_lat
 
 
-def conformal2geodetic(conformal_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def conformal2geodetic(conformal_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from conformal latitude to geodetic latitude
 
@@ -298,7 +309,7 @@ def conformal2geodetic(conformal_lat: float, ell: Ellipsoid = None, deg: bool =
     return degrees(geodetic_lat) if deg else geodetic_lat
 
 
-def geodetic2conformal(geodetic_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def geodetic2conformal(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from geodetic latitude to conformal latitude
 
@@ -344,7 +355,9 @@ def geodetic2conformal(geodetic_lat: float, ell: Ellipsoid = None, deg: bool = T
 
 
 # %% rectifying
-def geodetic2rectifying(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
+def geodetic2rectifying(
+    geodetic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True
+) -> float:
     """
     converts from geodetic latitude to rectifying latitude
 
@@ -438,9 +451,7 @@ def rectifying2geodetic(
 
 
 # %% authalic
-def geodetic2authalic(
-    geodetic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True
-) -> float:
+def geodetic2authalic(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from geodetic latitude to authalic latitude
 
@@ -484,9 +495,7 @@ def geodetic2authalic(
     return degrees(authalic_lat) if deg else authalic_lat
 
 
-def authalic2geodetic(
-    authalic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True
-) -> float:
+def authalic2geodetic(authalic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from authalic latitude to geodetic latitude
 
@@ -529,7 +538,7 @@ def authalic2geodetic(
 
 
 # %% parametric
-def geodetic2parametric(geodetic_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def geodetic2parametric(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     converts from geodetic latitude to parametric latitude
 
@@ -563,7 +572,9 @@ def geodetic2parametric(geodetic_lat: float, ell: Ellipsoid = None, deg: bool =
     return degrees(parametric_lat) if deg else parametric_lat
 
 
-def parametric2geodetic(parametric_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def parametric2geodetic(
+    parametric_lat: ndarray, ell: Ellipsoid = None, deg: bool = True
+) -> ndarray:
     """
     converts from parametric latitude to geodetic latitude
 


=====================================
src/pymap3d/los.py
=====================================
@@ -4,16 +4,17 @@ from __future__ import annotations
 import typing
 
 try:
-    from numpy import pi, nan, sqrt, ndarray, atleast_1d
+    from numpy import pi, nan, sqrt, atleast_1d
 except ImportError:
     from math import pi, nan, sqrt  # type: ignore
 
-    ndarray = typing.Any  # type: ignore
-
 from .aer import aer2enu
 from .ecef import enu2uvw, geodetic2ecef, ecef2geodetic
 from .ellipsoid import Ellipsoid
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["lookAtSpheroid"]
 
 


=====================================
src/pymap3d/lox.py
=====================================
@@ -4,14 +4,14 @@ from __future__ import annotations
 import typing
 
 try:
-    from numpy import radians, degrees, cos, arctan2 as atan2, tan, pi, ndarray, atleast_1d
+    from numpy import radians, degrees, cos, arctan2 as atan2, tan, array, broadcast_arrays
 except ImportError:
-    from math import radians, degrees, cos, atan2, tan, pi  # type: ignore
+    from math import radians, degrees, cos, atan2, tan  # type: ignore
 
-    ndarray = typing.Any  # type: ignore
+from math import pi, tau
 
-import typing
 from .ellipsoid import Ellipsoid
+from .utils import sign
 from . import rcurve
 from . import rsphere
 from .latitude import (
@@ -23,6 +23,9 @@ from .latitude import (
 )
 from .utils import sph2cart, cart2sph
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = [
     "loxodrome_inverse",
     "loxodrome_direct",
@@ -33,6 +36,9 @@ __all__ = [
 ]
 
 
+COS_EPS = 1e-9
+
+
 def meridian_dist(lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
     """
     Computes the ground distance on an ellipsoid from the equator to the input latitude.
@@ -138,6 +144,12 @@ def loxodrome_inverse(
     if deg:
         lat1, lon1, lat2, lon2 = radians(lat1), radians(lon1), radians(lat2), radians(lon2)
 
+    try:
+        lat1, lon1, lat2, lon2 = broadcast_arrays(lat1, lon1, lat2, lon2)
+
+    except NameError:
+        pass
+
     # compute changes in isometric latitude and longitude between points
     disolat = geodetic2isometric(lat2, deg=False, ell=ell) - geodetic2isometric(
         lat1, deg=False, ell=ell
@@ -146,35 +158,41 @@ def loxodrome_inverse(
 
     # compute azimuth
     az12 = atan2(dlon, disolat)
-    cosaz12 = cos(az12)
+    aux = abs(cos(az12))
 
     # compute distance along loxodromic curve
-    dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / abs(cos(az12))
+    dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / aux
+
+    # straight east or west
+    i = aux < COS_EPS
     try:
-        if (abs(cosaz12) < 1e-9).any():
-            dist[abs(cosaz12) < 1e-9] = departure(lon2, lon1, lat1, ell, deg=False)
+        dist[i] = departure(lon2[i], lon1[i], lat1[i], ell, deg=False)
     except (AttributeError, TypeError):
-        if abs(cosaz12) < 1e-9:  # straight east or west
+        if i:
             dist = departure(lon2, lon1, lat1, ell, deg=False)
 
     if deg:
         az12 = degrees(az12) % 360.0
 
-    return dist, az12
+    try:
+        return dist.squeeze()[()], az12.squeeze()[()]
+    except AttributeError:
+        return dist, az12
 
 
 def loxodrome_direct(
-    lat1: ndarray,
-    lon1: ndarray,
-    rng: ndarray,
+    lat1: float | ndarray,
+    lon1: float | ndarray,
+    rng: float | ndarray,
     a12: float,
     ell: Ellipsoid = None,
     deg: bool = True,
-) -> tuple[ndarray, ndarray]:
+) -> tuple[float | ndarray, float | ndarray]:
     """
     Given starting lat, lon with arclength and azimuth, compute final lat, lon
 
     like Matlab reckon('rh', ...)
+    except that "rng" in meters instead of "arclen" degrees of arc
 
     Parameters
     ----------
@@ -202,15 +220,16 @@ def loxodrome_direct(
     if deg:
         lat1, lon1, a12 = radians(lat1), radians(lon1), radians(a12)
 
+    a12 = a12 % tau
+
     try:
-        lat1 = atleast_1d(lat1)
-        rng = atleast_1d(rng)
-        if (abs(lat1) > pi / 2).any():
+        lat1, rng, a12 = broadcast_arrays(lat1, rng, a12)
+        if (abs(lat1) > pi / 2).any():  # type: ignore
             raise ValueError("-90 <= latitude <= 90")
-        if (rng < 0).any():
+        if (rng < 0).any():  # type: ignore
             raise ValueError("ground distance must be >= 0")
     except NameError:
-        if abs(lat1) > pi / 2:
+        if abs(lat1) > pi / 2:  # type: ignore
             raise ValueError("-90 <= latitude <= 90")
         if rng < 0:
             raise ValueError("ground distance must be >= 0")
@@ -226,14 +245,23 @@ def loxodrome_direct(
     newiso = geodetic2isometric(lat2, ell, deg=False)
     iso = geodetic2isometric(lat1, ell, deg=False)
 
+    # stability near singularities
+    i = abs(cos(a12)) < COS_EPS
     dlon = tan(a12) * (newiso - iso)
+
+    try:
+        dlon[i] = sign(pi - a12[i]) * rng[i] / rcurve.parallel(lat1[i], ell=ell, deg=False)  # type: ignore
+    except (AttributeError, TypeError):
+        if i:  # straight east or west
+            dlon = sign(pi - a12) * rng / rcurve.parallel(lat1, ell=ell, deg=False)
+
     lon2 = lon1 + dlon
 
     if deg:
         lat2, lon2 = degrees(lat2), degrees(lon2)
 
     try:
-        return lat2.squeeze()[()], lon2.squeeze()[()]
+        return lat2.squeeze()[()], lon2.squeeze()[()]  # type: ignore
     except AttributeError:
         return lat2, lon2
 
@@ -265,12 +293,12 @@ def departure(
     if deg:
         lon1, lon2, lat = radians(lon1), radians(lon2), radians(lat)
 
-    return rcurve.parallel(lat, ell, deg=False) * ((lon2 - lon1) % pi)
+    return rcurve.parallel(lat, ell=ell, deg=False) * ((lon2 - lon1) % pi)
 
 
 def meanm(
     lat: ndarray, lon: ndarray, ell: Ellipsoid = None, deg: bool = True
-) -> tuple[float | ndarray, float | ndarray]:
+) -> tuple[ndarray, ndarray]:
     """
     Computes geographic mean for geographic points on an ellipsoid
 
@@ -297,8 +325,8 @@ def meanm(
         lat, lon = radians(lat), radians(lon)
 
     lat = geodetic2authalic(lat, ell, deg=False)
-    assert isinstance(lat, ndarray)
-    x, y, z = sph2cart(lon, lat, 1)
+
+    x, y, z = sph2cart(lon, lat, array(1.0))
     lonbar, latbar, _ = cart2sph(x.sum(), y.sum(), z.sum())
     latbar = authalic2geodetic(latbar, ell, deg=False)
 


=====================================
src/pymap3d/ned.py
=====================================
@@ -3,15 +3,13 @@
 from __future__ import annotations
 import typing
 
-try:
-    from numpy import ndarray
-except ImportError:
-    ndarray = typing.Any  # type: ignore
-
 from .enu import geodetic2enu, aer2enu, enu2aer
 from .ecef import ecef2geodetic, ecef2enuv, ecef2enu, enu2ecef
 from .ellipsoid import Ellipsoid
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 
 def aer2ned(
     az: ndarray, elev: ndarray, slantRange: ndarray, deg: bool = True


=====================================
src/pymap3d/rcurve.py
=====================================
@@ -4,19 +4,20 @@ from __future__ import annotations
 import typing
 
 try:
-    from numpy import radians, sin, cos, sqrt, ndarray
+    from numpy import sin, cos, sqrt
 except ImportError:
-    from math import radians, sin, cos, sqrt  # type: ignore
-
-    ndarray = typing.Any  # type: ignore
+    from math import sin, cos, sqrt  # type: ignore
 
 from .ellipsoid import Ellipsoid
 from .utils import sanitize
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["parallel", "meridian", "transverse", "geocentric_radius"]
 
 
-def geocentric_radius(geodetic_lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def geocentric_radius(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """
     compute geocentric radius at geodetic latitude
 
@@ -36,10 +37,12 @@ def geocentric_radius(geodetic_lat: float, ell: Ellipsoid = None, deg: bool = Tr
     )
 
 
-def parallel(lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
+def parallel(lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
     """
     computes the radius of the small circle encompassing the globe at the specified latitude
 
+    like Matlab rcurve('parallel', ...)
+
     Parameters
     ----------
     lat : float
@@ -52,16 +55,15 @@ def parallel(lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
     Returns
     -------
     radius: float
-        radius of ellipsoid
+        radius of ellipsoid (meters)
     """
 
-    if deg:
-        lat = radians(lat)
+    lat, ell = sanitize(lat, ell, deg)
 
     return cos(lat) * transverse(lat, ell, deg=False)
 
 
-def meridian(lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
+def meridian(lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """computes the meridional radius of curvature for the ellipsoid
 
     like Matlab rcurve('meridian', ...)
@@ -81,17 +83,14 @@ def meridian(lat: float, ell: Ellipsoid = None, deg: bool = True) -> float:
         radius of ellipsoid
     """
 
-    if ell is None:
-        ell = Ellipsoid()
-    if deg:
-        lat = radians(lat)
+    lat, ell = sanitize(lat, ell, deg)
 
     f1 = ell.semimajor_axis * (1 - ell.eccentricity ** 2)
     f2 = 1 - (ell.eccentricity * sin(lat)) ** 2
     return f1 / sqrt(f2 ** 3)
 
 
-def transverse(lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True) -> float:
+def transverse(lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:
     """computes the radius of the curve formed by a plane
     intersecting the ellipsoid at the latitude which is
     normal to the surface of the ellipsoid
@@ -110,7 +109,7 @@ def transverse(lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True) ->
     Returns
     -------
     radius: float
-        radius of ellipsoid
+        radius of ellipsoid (meters)
     """
 
     lat, ell = sanitize(lat, ell, deg)


=====================================
src/pymap3d/rsphere.py
=====================================
@@ -4,17 +4,18 @@ from __future__ import annotations
 import typing
 
 try:
-    from numpy import radians, sin, cos, log, sqrt, degrees, asarray, ndarray
+    from numpy import radians, sin, cos, log, sqrt, degrees, asarray
 except ImportError:
     from math import radians, sin, cos, log, sqrt, degrees  # type: ignore
 
-    asarray = None
-    ndarray = typing.Any  # type: ignore
+    asarray = None  # type: ignore
 
 from .ellipsoid import Ellipsoid
 from . import rcurve
 from .vincenty import vdist
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
 
 __all__ = [
     "eqavol",
@@ -141,7 +142,7 @@ def euler(
     return rho * nu / den
 
 
-def curve(lat: float, ell: Ellipsoid = None, deg: bool = True, method: str = "mean") -> float:
+def curve(lat: ndarray, ell: Ellipsoid = None, deg: bool = True, method: str = "mean") -> ndarray:
     """computes the arithmetic average of the transverse and meridional
     radii of curvature at a specified latitude point
 


=====================================
src/pymap3d/sidereal.py
=====================================
@@ -1,6 +1,6 @@
 # Copyright (c) 2014-2018 Michael Hirsch, Ph.D.
 """ manipulations of sidereal time """
-from math import pi
+from math import tau
 from datetime import datetime
 
 from .timeconv import str2dt
@@ -128,4 +128,4 @@ def greenwichsrt(Jdate: float) -> float:
     )
 
     # 1/86400 and %(2*pi) implied by units of radians
-    return gmst_sec * (2 * pi) / 86400.0 % (2 * pi)
+    return gmst_sec * tau / 86400.0 % tau


=====================================
src/pymap3d/tests/test_latitude.py
=====================================
@@ -49,7 +49,14 @@ def test_numpy_geodetic_geocentric():
 
 @pytest.mark.parametrize(
     "geodetic_lat, isometric_lat",
-    [(0, 0), (90, inf), (-90, -inf), (45, 50.227466), (-45, -50.227466), (89, 271.275)],
+    [
+        (0, 0),
+        (90, inf),
+        (-90, -inf),
+        (45, 50.227466),
+        (-45, -50.227466),
+        (89, 271.275),
+    ],
 )
 def test_geodetic_isometric(geodetic_lat, isometric_lat):
     isolat = latitude.geodetic2isometric(geodetic_lat)


=====================================
src/pymap3d/tests/test_rcurve.py
=====================================
@@ -41,4 +41,4 @@ def test_numpy_meridian():
 
 def test_numpy_transverse():
     pytest.importorskip("numpy")
-    assert rcurve.transverse([0, 90]) == approx([A, 6399593.626])
+    assert rcurve.transverse([-90, 0, 90]) == approx([6399593.6258, A, 6399593.6258])


=====================================
src/pymap3d/tests/test_rhumb.py
=====================================
@@ -69,15 +69,36 @@ def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az):
 def test_numpy_loxodrome_inverse():
     pytest.importorskip("numpy")
     d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, -148)
+    assert d == approx(5248666.209)
+    assert a == approx(302.00567)
+
+    d, a = lox.loxodrome_inverse([40, 40], [-80, -80], [65, 65], -148)
+    d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, [-148, -148])
 
+
+def test_numpy_2d_loxodrome_inverse():
+    pytest.importorskip("numpy")
+    d, a = lox.loxodrome_inverse([[40, 40], [40, 40]], [[-80, -80], [-80, -80]], 65, -148)
     assert d == approx(5248666.209)
     assert a == approx(302.00567)
 
+    d, a = lox.loxodrome_inverse(
+        [[40, 40], [40, 40]], [[-80, -80], [-80, -80]], [[65, 65], [65, 65]], -148
+    )
+    d, a = lox.loxodrome_inverse(
+        [[40, 40], [40, 40]], [[-80, -80], [-80, -80]], 65, [[-148, -148], [-148, -148]]
+    )
+    d, a = lox.loxodrome_inverse(40, -80, [[65, 65], [65, 65]], [[-148, -148], [-148, -148]])
+
 
 @pytest.mark.parametrize(
     "lat0,lon0,rng,az,lat1,lon1",
     [
-        (40, -80, 10000, 30, 40.077995, -79.9414144),
+        (40, -80, 10000, 30, 40.0000779959676, -79.9999414477481),
+        (35, 140, 50000, 90, 35, 140.548934481815),
+        (35, 140, 50000, -270, 35, 140.548934481815),
+        (35, 140, 50000, 270, 35, 139.451065518185),
+        (35, 140, 50000, -90, 35, 139.451065518185),
         (0, 0, 0, 0, 0, 0),
         (0, 0, 10018754.17, 90, 0, 90),
         (0, 0, 10018754.17, -90, 0, -90),
@@ -86,18 +107,25 @@ def test_numpy_loxodrome_inverse():
     ],
 )
 def test_loxodrome_direct(lat0, lon0, rng, az, lat1, lon1):
+    """
+    reckon('rh', 40, -80, 10, 30, wgs84Ellipsoid)
+    """
     lat2, lon2 = lox.loxodrome_direct(lat0, lon0, rng, az)
-    assert lat2 == approx(lat1, abs=1e-6)
-    assert lon2 == approx(lon1)
+    assert lat2 == approx(lat1, rel=0.005, abs=1e-6)
+    assert lon2 == approx(lon1, rel=0.001)
     assert isinstance(lat2, float)
     assert isinstance(lon2, float)
 
 
 def test_numpy_loxodrome_direct():
     pytest.importorskip("numpy")
-    lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10000, 10000], [30, 30])
-    assert lat == approx(40.077995)
-    assert lon == approx(-79.941414)
+    lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], [30, 30])
+    assert lat == approx(40.000078)
+    assert lon == approx(-79.99994145)
+
+    lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, 30)
+    lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], 30)
+    lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, [30, 30])
 
 
 @pytest.mark.parametrize("lat,lon", [([0, 45, 90], [0, 45, 90])])


=====================================
src/pymap3d/timeconv.py
=====================================
@@ -5,11 +5,11 @@ from datetime import datetime
 try:
     from dateutil.parser import parse
 except ImportError:
-    parse = None
+    parse = None  # type: ignore
 try:
     import numpy as np
 except ImportError:
-    np = None
+    np = None  # type: ignore
 
 
 __all__ = ["str2dt"]


=====================================
src/pymap3d/utils.py
=====================================
@@ -8,12 +8,11 @@ import typing
 from .ellipsoid import Ellipsoid
 
 try:
-    from numpy import hypot, cos, sin, arctan2 as atan2, radians, pi, asarray, ndarray, sign
+    from numpy import hypot, cos, sin, arctan2 as atan2, radians, pi, asarray, sign
 except ImportError:
     from math import atan2, hypot, cos, sin, radians, pi  # type: ignore
 
-    asarray = None
-    ndarray = typing.Any  # type: ignore
+    asarray = None  # type: ignore
 
     def sign(x: float) -> float:  # type: ignore
         """signum function"""
@@ -27,6 +26,9 @@ except ImportError:
         return y
 
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["cart2pol", "pol2cart", "cart2sph", "sph2cart", "sign"]
 
 
@@ -40,9 +42,7 @@ def pol2cart(theta: float, rho: float) -> tuple[float, float]:
     return rho * cos(theta), rho * sin(theta)
 
 
-def cart2sph(
-    x: float | ndarray, y: float | ndarray, z: float | ndarray
-) -> tuple[float | ndarray, float | ndarray, float | ndarray]:
+def cart2sph(x: ndarray, y: ndarray, z: ndarray) -> tuple[ndarray, ndarray, ndarray]:
     """Transform Cartesian to spherical coordinates"""
     hxy = hypot(x, y)
     r = hypot(hxy, z)
@@ -51,9 +51,7 @@ def cart2sph(
     return az, el, r
 
 
-def sph2cart(
-    az: float | ndarray, el: float | ndarray, r: float | ndarray
-) -> tuple[float | ndarray, float | ndarray, float | ndarray]:
+def sph2cart(az: ndarray, el: ndarray, r: ndarray) -> tuple[ndarray, ndarray, ndarray]:
     """Transform spherical to Cartesian coordinates"""
     rcos_theta = r * cos(el)
     x = rcos_theta * cos(az)
@@ -62,7 +60,9 @@ def sph2cart(
     return x, y, z
 
 
-def sanitize(lat: float | ndarray, ell: Ellipsoid, deg: bool) -> tuple[float | ndarray, Ellipsoid]:
+def sanitize(
+    lat: float | ndarray, ell: typing.Optional[Ellipsoid], deg: bool
+) -> tuple[float | ndarray, Ellipsoid]:
     if ell is None:
         ell = Ellipsoid()
     if asarray is not None:
@@ -71,10 +71,10 @@ def sanitize(lat: float | ndarray, ell: Ellipsoid, deg: bool) -> tuple[float | n
         lat = radians(lat)
 
     if asarray is not None:
-        if (abs(lat) > pi / 2).any():
+        if (abs(lat) > pi / 2).any():  # type: ignore
             raise ValueError("-pi/2 <= latitude <= pi/2")
     else:
-        if abs(lat) > pi / 2:
+        if abs(lat) > pi / 2:  # type: ignore
             raise ValueError("-pi/2 <= latitude <= pi/2")
 
     return lat, ell


=====================================
src/pymap3d/vincenty.py
=====================================
@@ -10,7 +10,6 @@ from copy import copy
 
 try:
     from numpy import (
-        ndarray,
         atleast_1d,
         sqrt,
         tan,
@@ -26,11 +25,12 @@ try:
 except ImportError:
     from math import sqrt, tan, sin, cos, isnan, atan, atan2, asin, radians, degrees  # type: ignore
 
-    ndarray = typing.Any  # type: ignore
-
 from .ellipsoid import Ellipsoid
 from .utils import sign
 
+if typing.TYPE_CHECKING:
+    from numpy import ndarray
+
 __all__ = ["vdist", "vreckon", "track2"]
 
 
@@ -123,7 +123,7 @@ def vdist(
         if (abs(Lat1) > 90).any() | (abs(Lat2) > 90).any():
             raise ValueError("Input latitudes must be in [-90, 90] degrees.")
     except NameError:
-        if (abs(Lat1) > 90) | (abs(Lat2) > 90):
+        if (abs(Lat1) > 90) | (abs(Lat2) > 90):  # type: ignore
             raise ValueError("Input latitudes must be in [-90, 90] degrees.")
     # %% Supply WGS84 earth ellipsoid axis lengths in meters:
     a = ell.semimajor_axis
@@ -158,7 +158,7 @@ def vdist(
         L[L > pi] = 2 * pi - L[L > pi]
     except TypeError:
         if L > pi:
-            L = 2 * pi - L
+            L = 2 * pi - L  # type: ignore
 
     lamb = copy(L)  # NOTE: program will fail without copy!
     itercount = 0
@@ -170,7 +170,7 @@ def vdist(
             if not warninggiven:
                 logging.warning("Essentially antipodal points--precision may be reduced slightly.")
 
-            lamb = pi
+            lamb = pi  # type: ignore
             break
 
         lambdaold = copy(lamb)
@@ -216,7 +216,7 @@ def vdist(
         # print(f'then, lambda(21752) = {lamb[21752],20})
         # correct for convergence failure for essentially antipodal points
         try:
-            i = (lamb > pi).any()
+            i = (lamb > pi).any()  # type: ignore
         except AttributeError:
             i = lamb > pi
 
@@ -225,11 +225,11 @@ def vdist(
                 "Essentially antipodal points encountered. Precision may be reduced slightly."
             )
             warninggiven = True
-            lambdaold = pi
-            lamb = pi
+            lambdaold = pi  # type: ignore
+            lamb = pi  # type: ignore
 
         try:
-            notdone = (abs(lamb - lambdaold) > 1e-12).any()
+            notdone = (abs(lamb - lambdaold) > 1e-12).any()  # type: ignore
         except AttributeError:
             notdone = abs(lamb - lambdaold) > 1e-12
 
@@ -341,14 +341,14 @@ def vreckon(
         Lon1 = atleast_1d(Lon1)
         Rng = atleast_1d(Rng)
         Azim = atleast_1d(Azim)
-        if (abs(Lat1) > 90).any():
+        if (abs(Lat1) > 90.0).any():
             raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.")
-        if (Rng < 0).any():
+        if (Rng < 0.0).any():
             raise ValueError("Ground distance must be positive")
     except NameError:
-        if abs(Lat1) > 90:
+        if abs(Lat1) > 90.0:  # type: ignore
             raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.")
-        if Rng < 0:
+        if Rng < 0.0:
             raise ValueError("Ground distance must be positive")
 
     if ell is not None:


=====================================
src/pymap3d/vreckon/__main__.py
=====================================
@@ -13,4 +13,4 @@ P = p.parse_args()
 
 lat2, lon2 = vincenty.vreckon(P.lat, P.lon, P.range, P.azimuth)
 
-print("lat, lon = ({:.4f}, {:.4f})".format(lat2.item(), lon2.item()))
+print(f"lat, lon = ({lat2.item():.4f}, {lon2.item():.4f})")



View it on GitLab: https://salsa.debian.org/debian-gis-team/pymap3d/-/commit/6e091f6a78b215283bec1d611c6593006b5ff35f

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pymap3d/-/commit/6e091f6a78b215283bec1d611c6593006b5ff35f
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20211020/c32d903d/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list