pax_global_header00006660000000000000000000000064141466341540014522gustar00rootroot0000000000000052 comment=6b1978891bbfb41eabacfdf81db7d18a31c06792 pymap3d-2.7.3/000077500000000000000000000000001414663415400131105ustar00rootroot00000000000000pymap3d-2.7.3/.coveragerc000066400000000000000000000011521414663415400152300ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] branch = True source = src/ omit = archive/* */.local/* [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: ignore_errors = True [html] directory = coverage_html_report pymap3d-2.7.3/.flake8000066400000000000000000000002511414663415400142610ustar00rootroot00000000000000[flake8] max-line-length = 132 ignore = E501, W503, W504 exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/ per-file-ignores = __init__.py:F401, F403pymap3d-2.7.3/.gitattributes000066400000000000000000000006351414663415400160070ustar00rootroot00000000000000archive/* linguist-documentation docs/* linguist-documentation paper/* linguist-documentation .gitattributes text eol=lf .gitignore text eol=lf Makefile text eol=lf *.yml text eol=lf LICENSE text eol=lf *.ipynb text eol=lf *.txt text eol=lf *.py text eol=lf *.sh text eol=lf *.c text eol=lf *.cpp text eol=lf *.f text eol=lf *.f90 text eol=lf *.md text eol=lf *.rst text eol=lf *.csv text eol=lf *.m text eol=lf pymap3d-2.7.3/.github/000077500000000000000000000000001414663415400144505ustar00rootroot00000000000000pymap3d-2.7.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001414663415400166335ustar00rootroot00000000000000pymap3d-2.7.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000002601414663415400213230ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** What is your expected output value(s)? What program/function are you comparing with? pymap3d-2.7.3/.github/contributors.md000066400000000000000000000003551414663415400175320ustar00rootroot00000000000000Thanks to those who contributed code and ideas, including: ``` @aldebaran1 (robustness) @rpavlick (multiple features + functions) @cchuravy (Ellipsoid parameters) @jprMesh (more conversion functions) @Fil (docs) @SamuelMarks (docs) ```pymap3d-2.7.3/.github/workflows/000077500000000000000000000000001414663415400165055ustar00rootroot00000000000000pymap3d-2.7.3/.github/workflows/ci.yml000066400000000000000000000011141414663415400176200ustar00rootroot00000000000000name: ci on: push: paths: - "**.py" - .github/workflows/ci.yml pull-request: - "**.py" - .github/workflows/ci.yml jobs: full: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: 3.9 - run: pip install .[full,tests,lint] - run: flake8 - run: mypy - run: pytest # codecov coverage # - run: pip install codecov pytest-cov # - run: pytest --cov --cov-report=xml # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v1 pymap3d-2.7.3/.github/workflows/ci_stdlib_only.yml000066400000000000000000000010351414663415400222240ustar00rootroot00000000000000name: ci_stdlib_only on: push: 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@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - run: pip install .[tests] - run: pytest pymap3d-2.7.3/.gitignore000066400000000000000000000014161414663415400151020ustar00rootroot00000000000000.ipynb_checkpoints/ .eggs/ .mypy_cache/ .pytest_cache/ bin/ doctrees/ .buildinfo *.out *.mod *.smod *~ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ pymap3d-2.7.3/.lgtm.yml000066400000000000000000000000711414663415400146520ustar00rootroot00000000000000extraction: python: python_setup: version: 3 pymap3d-2.7.3/.mypy.ini000066400000000000000000000002601414663415400146630ustar00rootroot00000000000000[mypy] files = src/, Examples/, scripts/ ignore_missing_imports = True strict_optional = False allow_redefinition = True show_error_context = False show_column_numbers = True pymap3d-2.7.3/.nojekyll000066400000000000000000000000001414663415400147260ustar00rootroot00000000000000pymap3d-2.7.3/CITATION000066400000000000000000000000471414663415400142460ustar00rootroot00000000000000https://doi.org/10.5281/zenodo.3262738 pymap3d-2.7.3/Examples/000077500000000000000000000000001414663415400146665ustar00rootroot00000000000000pymap3d-2.7.3/Examples/angle_distance.py000077500000000000000000000014441414663415400202060ustar00rootroot00000000000000#!/usr/bin/env python from pymap3d.haversine import anglesep, anglesep_meeus from argparse import ArgumentParser from pytest import approx def main(): p = ArgumentParser(description="angular distance between two sky points") p.add_argument("r0", help="right ascension: first point [deg]", type=float) p.add_argument("d0", help="declination: first point [deg]", type=float) p.add_argument("r1", help="right ascension: 2nd point [deg]", type=float) p.add_argument("d1", help="declination: 2nd point [degrees]", type=float) a = p.parse_args() 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(f"{dist_deg:.6f} deg sep") assert dist_deg == approx(dist_deg_astropy) if __name__ == "__main__": main() pymap3d-2.7.3/Examples/azel2radec.py000077500000000000000000000015411414663415400172600ustar00rootroot00000000000000#!/usr/bin/env python """ Example Kitt Peak ./demo_azel2radec.py 264.9183 37.911388 31.9583 -111.597 2014-12-25T22:00:00MST """ from pymap3d import azel2radec from argparse import ArgumentParser def main(): p = ArgumentParser( description="convert azimuth and elevation to " "right ascension and declination" ) p.add_argument("azimuth", help="azimuth [deg]", type=float) p.add_argument("elevation", help="elevation [deg]", type=float) p.add_argument("lat", help="WGS84 obs. lat [deg]", type=float) p.add_argument("lon", help="WGS84 obs. lon [deg]", type=float) p.add_argument("time", help="obs. time YYYY-mm-ddTHH:MM:SSZ") P = p.parse_args() ra, dec = azel2radec(P.azimuth, P.elevation, P.lat, P.lon, P.time) print("ra [deg] ", ra, " dec [deg] ", dec) if __name__ == "__main__": # pragma: no cover main() pymap3d-2.7.3/Examples/geodetic to ENU.ipynb000066400000000000000000000014331414663415400205300ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pymap3d as pm\n", "lat0, lon0, alt0 = 4029.25745, 11146.61129, 1165.2\n", "lat, lon, alt = 4028.25746, 11147.61125, 1165.1\n", "e, n, u = pm.geodetic2enu(lat, lon, alt, lat0, lon0, alt0)\n", "print(e, n, u)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 4 } pymap3d-2.7.3/Examples/has_map_toolbox.m000066400000000000000000000001131414663415400202150ustar00rootroot00000000000000function has_map = has_map_toolbox() has_map = ~isempty(ver("map")); end pymap3d-2.7.3/Examples/lox_stability.py000066400000000000000000000024351414663415400201320ustar00rootroot00000000000000#!/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) pymap3d-2.7.3/Examples/plot_geodetic2ecef.py000066400000000000000000000021741414663415400207720ustar00rootroot00000000000000#!/usr/bin/env python3 import pymap3d as pm import matplotlib.pyplot as mpl import numpy as np import argparse p = argparse.ArgumentParser() p.add_argument("alt_m", help="altitude [meters]", type=float, default=0.0, nargs="?") p = p.parse_args() lat, lon = np.meshgrid(np.arange(-90, 90, 0.1), np.arange(-180, 180, 0.2)) x, y, z = pm.geodetic2ecef(lat, lon, p.alt_m) def panel(ax, val, name: str, cmap: str = None): hi = ax.pcolormesh(lon, lat, val, cmap=cmap) ax.set_title(name) fg.colorbar(hi, ax=ax).set_label(name + " [m]") ax.set_xlabel("longitude [deg]") fg = mpl.figure(figsize=(16, 5)) axs = fg.subplots(1, 3, sharey=True) fg.suptitle("geodetic2ecef") panel(axs[0], x, "x", "bwr") panel(axs[1], y, "y", "bwr") panel(axs[2], z, "z", "bwr") axs[0].set_ylabel("latitude [deg]") fg = mpl.figure(figsize=(16, 5)) axs = fg.subplots(1, 3, sharey=True) fg.suptitle(r"|$\nabla$ geodetic2ecef|") panel(axs[0], np.hypot(*np.gradient(x)), r"|$\nabla$ x|") panel(axs[1], np.hypot(*np.gradient(y)), r"|$\nabla$ y|") panel(axs[2], np.hypot(*np.gradient(z)), r"|$\nabla$ z|") axs[0].set_ylabel("latitude [deg]") mpl.show() pymap3d-2.7.3/Examples/radec2azel.py000077500000000000000000000015701414663415400172620ustar00rootroot00000000000000#!/usr/bin/env python """ Example Kitt Peak ./radec2azel.py 257.96295344 15.437854 31.9583 -111.5967 2014-12-25T22:00:00MST """ from pymap3d import radec2azel from argparse import ArgumentParser def main(): p = ArgumentParser(description="RightAscension,Declination =>" "Azimuth,Elevation") p.add_argument("ra", help="right ascension [degrees]", type=float) p.add_argument("dec", help="declination [degrees]", type=float) p.add_argument("lat", help="WGS84 latitude of observer [degrees]", type=float) p.add_argument("lon", help="WGS84 latitude of observer [degrees]", type=float) p.add_argument("time", help="UTC time of observation YYYY-mm-ddTHH:MM:SSZ") P = p.parse_args() az_deg, el_deg = radec2azel(P.ra, P.dec, P.lat, P.lon, P.time) print("azimuth: [deg]", az_deg) print("elevation [deg]:", el_deg) if __name__ == "__main__": main() pymap3d-2.7.3/Examples/vdist_poi.py000066400000000000000000000044121414663415400172410ustar00rootroot00000000000000#!/usr/bin/env python """ Example of using Google Maps queries and PyMap3D https://developers.google.com/places/web-service/search This requires a Google Cloud key, and costs a couple US cents per query. TODO: Would like to instead query a larger region, would OSM be an option? """ from pathlib import Path from argparse import ArgumentParser import requests import functools import pandas from pymap3d.vincenty import vdist URL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?" @functools.lru_cache() def get_place_coords( place_type: str, latitude: float, longitude: float, search_radius_km: int, keyfn: Path ) -> pandas.DataFrame: """ Get places using Google Maps Places API Requires you to have a Google Cloud account with API key. """ keyfn = Path(keyfn).expanduser() key = keyfn.read_text() stub = URL + f"location={latitude},{longitude}" stub += f"&radius={search_radius_km * 1000}" stub += f"&types={place_type}" stub += f"&key={key}" r = requests.get(stub) r.raise_for_status() place_json = r.json()["results"] places = pandas.DataFrame( index=[p["name"] for p in place_json], columns=["latitude", "longitude", "distance_km", "vicinity"], ) places["latitude"] = [p["geometry"]["location"]["lat"] for p in place_json] places["longitude"] = [p["geometry"]["location"]["lng"] for p in place_json] places["vicinity"] = [p["vicinity"] for p in place_json] return places if __name__ == "__main__": p = ArgumentParser() p.add_argument( "place_type", help="Place type to search: https://developers.google.com/places/supported_types", ) p.add_argument( "searchloc", help="initial latituude, longitude to search from", nargs=2, type=float ) p.add_argument("radius", help="search radius (kilometers)", type=int) p.add_argument("refloc", help="reference location (lat, lon)", nargs=2, type=float) p.add_argument("-k", "--keyfn", help="Google Places API key file", default="~/googlemaps.key") a = p.parse_args() place_coords = get_place_coords(a.place_type, *a.searchloc, a.radius, a.keyfn) place_coords["distance_km"] = ( vdist(place_coords["latitude"], place_coords["longitude"], *a.refloc)[0] / 1e3 ) pymap3d-2.7.3/Examples/vdist_stability.py000066400000000000000000000027741414663415400204670ustar00rootroot00000000000000#!/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 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, lon1, lat2, lon2) -> tuple[float, float]: """Using Matlab Engine to do same thing as Pymap3d""" return eng.distance(lat1, lon1, lat2, lon2, eng.wgs84Ellipsoid(), nargout=2) # type: ignore dlast, alast = nan, nan lon1, lon2 = 0.0, 1.0 for i in range(20): lat1 = lat2 = 10.0 ** (-i) dist_m, az_deg = vdist(lat1, lon1, lat2, lon2) assert dist_m != dlast assert az_deg != alast mat_match = True dist_matlab, az_matlab = matlab_func(lat1, lon1, lat2, lon2) if not isclose(dist_matlab, dist_m): mat_match = False print( f"MISMATCH: latitude {lat1} {lat2}: Python: {dist_m} Matlab: {dist_matlab}", file=sys.stderr, ) if not isclose(az_matlab, az_deg): mat_match = False print( f"MISMATCH: latitude {lat1} {lat2}: Python: {az_matlab} Matlab: {az_deg}", file=sys.stderr, ) if mat_match: print(f"latitudes {lat1} {lat2}: {dist_m} meters {az_deg} deg azimuth") pymap3d-2.7.3/LICENSE.txt000066400000000000000000000025261414663415400147400ustar00rootroot00000000000000Copyright (c) 2014-2018 Michael Hirsch, Ph.D. Copyright (c) 2013, Felipe Geremia Nievinski Copyright (c) 2004-2007 Michael Kleder Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.pymap3d-2.7.3/README.md000066400000000000000000000142361414663415400143750ustar00rootroot00000000000000# Python 3-D coordinate conversions [![image](https://zenodo.org/badge/DOI/10.5281/zenodo.213676.svg)](https://doi.org/10.5281/zenodo.213676) [![image](http://joss.theoj.org/papers/10.21105/joss.00580/status.svg)](https://doi.org/10.21105/joss.00580) [![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) [![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) Pure Python (no prerequistes beyond Python itself) 3-D geographic coordinate conversions and geodesy. API similar to popular $1000 Matlab Mapping Toolbox routines for Python PyMap3D is intended for non-interactive use on massively parallel (HPC) and embedded systems. [API docs](https://geospace-code.github.io/pymap3d/) 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/maptran3d) * [Rust](https://github.com/gberrante/map_3d) ## Prerequisites Pymap3d is compatible with Python ≥ 3.7 including PyPy. Numpy and AstroPy are optional; algorithms from Vallado and Meeus are used if AstroPy is not present. ## Install ```sh python3 -m pip install pymap3d ``` or for the latest development code: ```sh git clone https://github.com/geospace-code/pymap3d pip install -e pymap3d ``` One can verify Python functionality after installation by: ```sh pytest pymap3d ``` ## Usage Where consistent with the definition of the functions, all arguments may be arbitrarily shaped (scalar, N-D array). ```python import pymap3d as pm x,y,z = pm.geodetic2ecef(lat,lon,alt) 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/tutorial/controlflow.html#unpacking-argument-lists) can be used for compact function arguments with scalars or arbitrarily shaped N-D arrays: ```python aer = (az,el,slantrange) obslla = (obs_lat,obs_lon,obs_alt) 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. Native Python float is typically [64 bit](https://docs.python.org/3/library/stdtypes.html#typesnumeric). Numpy can select real precision bits: 32, 64, 128, etc. ### Functions Popular mapping toolbox functions ported to Python include the following, where the source coordinate system (before the "2") is converted to the desired coordinate system: aer2ecef aer2enu aer2geodetic aer2ned ecef2aer ecef2enu ecef2enuv ecef2geodetic ecef2ned ecef2nedv ecef2eci eci2ecef eci2aer aer2eci geodetic2eci eci2geodetic enu2aer enu2ecef enu2geodetic geodetic2aer geodetic2ecef geodetic2enu geodetic2ned ned2aer ned2ecef ned2geodetic azel2radec radec2azel vreckon vdist lookAtSpheroid track2 departure meanm rcurve rsphere geod2geoc geoc2geod Additional functions: * loxodrome_inverse: rhumb line distance and azimuth between ellipsoid points (lat,lon) akin to Matlab `distance('rh', ...)` and `azimuth('rh', ...)` * loxodrome_direct * geodetic latitude transforms to/from: parametric, authalic, isometric, and more in pymap3d.latitude Abbreviations: * [AER: Azimuth, Elevation, Range](https://en.wikipedia.org/wiki/Spherical_coordinate_system) * [ECEF: Earth-centered, Earth-fixed](https://en.wikipedia.org/wiki/ECEF) * [ECI: Earth-centered Inertial using IERS](https://www.iers.org/IERS/EN/Home/home_node.html) via `astropy` * [ENU: East North Up](https://en.wikipedia.org/wiki/Axes_conventions#Ground_reference_frames:_ENU_and_NED) * [NED: North East Down](https://en.wikipedia.org/wiki/North_east_down) * [radec: right ascension, declination](https://en.wikipedia.org/wiki/Right_ascension) ### command line Command line convenience functions provided include: ```sh python -m pymap3d.vdist python -m pymap3d.vreckon ``` ### array vs scalar Use of pymap3d on embedded systems or other streaming data applications often deal with scalar position data. These data are handled efficiently with the Python math stdlib module. Vector data can be handled via list comprehension. Those needing multidimensional data with SIMD and other Numpy and/or PyPy accelerated performance can do so automatically by installing Numpy. pymap3d seamlessly falls back to Python's math module if Numpy isn't present. To keep the code clean, only scalar data can be used without Numpy. As noted above, use list comprehension if you need vector data without Numpy. ### Caveats * Atmospheric effects neglected in all functions not invoking AstroPy. Would need to update code to add these input parameters (just start a GitHub Issue to request). * Planetary perturbations and nutation etc. not fully considered. ## Notes As compared to [PyProj](https://github.com/jswhit/pyproj): * PyMap3D does not require anything beyond pure Python for most transforms * Astronomical conversions are done using (optional) AstroPy for established accuracy * PyMap3D API is similar to Matlab Mapping Toolbox, while PyProj's interface is quite distinct * PyMap3D intrinsically handles local coordinate systems such as ENU, while PyProj ENU requires some [additional effort](https://github.com/jswhit/pyproj/issues/105). * PyProj is oriented towards points on the planet surface, while PyMap3D handles points on or above the planet surface equally well, particularly important for airborne vehicles and remote sensing. ### AstroPy.Units.Quantity At this time, [AstroPy.Units.Quantity](http://docs.astropy.org/en/stable/units/) is not supported. Let us know if this is of interest. Impacts on performance would have to be considered before making Quantity a first-class citizen. For now, you can workaround by passing in the `.value` of the variable. pymap3d-2.7.3/codemeta.json000066400000000000000000000020441414663415400155640ustar00rootroot00000000000000{ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/BSD-2-Clause", "codeRepository": "https://github.com/geospace-code/pymap3d", "contIntegration": "https://github.com/geospace-code/pymap3d/actions", "dateCreated": "2014-08-01", "datePublished": "2014-08-03", "dateModified": "2020-05-10", "issueTracker": "https://github.com/geospace-code/pymap3d/issues", "name": "PyMap3d", "version": "2.4.1", "identifier": "10.5281/zenodo.3262738", "description": "pure-Python (Numpy optional) 3D coordinate conversions for geospace", "applicationCategory": "geospace", "developmentStatus": "active", "funder": { "@type": "Organization", "name": "AFOSR" }, "keywords": [ "coordinate transformation" ], "programmingLanguage": [ "Python" ], "author": [ { "@type": "Person", "@id": "https://orcid.org/0000-0002-1637-6526", "givenName": "Michael", "familyName": "Hirsch" } ] } pymap3d-2.7.3/paper/000077500000000000000000000000001414663415400142175ustar00rootroot00000000000000pymap3d-2.7.3/paper/codemeta.json000066400000000000000000000012041414663415400166700ustar00rootroot00000000000000{ "@context": "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type": "Code", "author": [ { "@id": "", "@type": "Person", "email": "", "name": "Michael Hirsch, Ph.D", "affiliation": "" } ], "identifier": "http://doi.org/10.5281/zenodo.213676", "codeRepository": "https://github.com/scivision/pymap3d", "datePublished": "2018-01-28", "dateModified": "2018-01-28", "dateCreated": "2018-01-28", "description": "3-D coordinate conversions in pure Python", "keywords": "geospace,coordinates", "license": "BSD", "title": "pymap3d", "version": "1.4.0" } pymap3d-2.7.3/paper/generate.rb000077500000000000000000000114101414663415400163360ustar00rootroot00000000000000#!/usr/bin/ruby # https://gist.github.com/arfon/478b2ed49e11f984d6fb # For an OO language, this is distinctly procedural. Should probably fix that. require 'json' details = Hash.new({}) capture_params = [ { :name => "title", :message => "Enter project name." }, { :name => "url", :message => "Enter the URL of the project repository." }, { :name => "description", :message => "Enter the (short) project description." }, { :name => "license", :message => "Enter the license this software shared under. (hit enter to skip)\nFor example MIT, BSD, GPL v3.0, Apache 2.0" }, { :name => "doi", :message => "Enter the DOI of the archived version of this code. (hit enter to skip)\nFor example http://dx.doi.org/10.6084/m9.figshare.828487" }, { :name => "keywords", :message => "Enter keywords that should be associated with this project (hit enter to skip)\nComma-separated, for example: turkey, chicken, pot pie" }, { :name => "version", :message => "Enter the version of your software (hit enter to skip)\nSEMVER preferred: http://semver.org e.g. v1.0.0" } ] puts "I'm going to try and help you prepare some things for your JOSS submission" puts "If all goes well then we'll have a nice codemeta.json file soon..." puts "" puts "************************************" puts "* First, some basic details *" puts "************************************" puts "" # Loop through the desired captures and print out for clarity capture_params.each do |param| puts param[:message] print "> " input = gets details[param[:name]] = input.chomp puts "" puts "OK, your project has #{param[:name]}: #{input}" puts "" end puts "" puts "************************************" puts "* Experimental stuff *" puts "************************************" puts "" puts "Would you like me to try and build a list of authors for you?" puts "(You need to be running this script in a git repository for this to work)" print "> (Y/N)" answer = gets.chomp case answer.downcase when "y", "yes" # Use git shortlog to extract a list of author names and commit counts. # Note we don't extract emails here as there's often different emails for # each user. Instead we capture emails at the end. git_log = `git shortlog --summary --numbered --no-merges` # ["252\tMichael Jackson", "151\tMC Hammer"] authors_and_counts = git_log.split("\n").map(&:strip) authors_and_counts.each do |author_count| count, author = author_count.split("\t").map(&:strip) puts "Looks like #{author} made #{count} commits" puts "Add them to the output?" print "> (Y/N)" answer = gets.chomp # If a user chooses to add this author to the output then we ask for some # additional information including their email, ORCID and affiliation. case answer.downcase when "y", "yes" puts "What is #{author}'s email address? (hit enter to skip)" print "> " email = gets.chomp puts "What is #{author}'s ORCID? (hit enter to skip)" puts "For example: http://orcid.org/0000-0000-0000-0000" print "> " orcid = gets.chomp puts "What is #{author}'s affiliation? (hit enter to skip)" print "> " affiliation = gets.chomp details['authors'].merge!(author => { 'commits' => count, 'email' => email, 'orcid' => orcid, 'affiliation' => affiliation }) when "n", "no" puts "OK boss..." puts "" end end when "n", "no" puts "OK boss..." puts "" end puts "Reticulating splines" 5.times do print "." sleep 0.5 end puts "" puts "Generating some JSON goodness..." # TODO: work out how to use some kind of JSON template here. # Build the output list of authors from the inputs we've collected. output_authors = [] details['authors'].each do |author_name, values| entry = { "@id" => values['orcid'], "@type" => "Person", "email" => values['email'], "name" => author_name, "affiliation" => values['affiliation'] } output_authors << entry end # TODO: this is currently a static template (written out here). It would be good # to do something smarter here. output = { "@context" => "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type" => "Code", "author" => output_authors, "identifier" => details['doi'], "codeRepository" => details['url'], "datePublished" => Time.now.strftime("%Y-%m-%d"), "dateModified" => Time.now.strftime("%Y-%m-%d"), "dateCreated" => Time.now.strftime("%Y-%m-%d"), "description" => details['description'], "keywords" => details['keywords'], "license" => details['license'], "title" => details['title'], "version" => details['version'] } File.open('codemeta.json', 'w') {|f| f.write(JSON.pretty_generate(output)) } pymap3d-2.7.3/paper/paper.bib000066400000000000000000000021401414663415400160010ustar00rootroot00000000000000 @article{vincenty, author = {T. Vincenty}, title = {Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations}, journal = {Survey Review}, volume = 23, number = 176, year = 1975, month = 4, pages = {88--93}, url = {https://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf}, } @online{veness, author = {Chris Veness}, title = {Vincenty solutions of geodesics on the ellipsoid}, year = 2016, url = {http://www.movable-type.co.uk/scripts/latlong-vincenty.html#direct}, } @online{pymap3d, author = {Michael Hirsch}, title = {PyMap3D: 3-D coordinate conversion software for Python and Matlab}, year = 2018, url = {https://github.com/scivision/pymap3d}, doi = {https://doi.org/10.5281/zenodo.595430}, } @ARTICLE{7368896, author={M. Hirsch and J. Semeter and M. Zettergren and H. Dahlgren and C. Goenka and H. Akbari}, journal={IEEE Transactions on Geoscience and Remote Sensing}, title={Reconstruction of Fine-Scale Auroral Dynamics}, year={2016}, volume={54}, number={5}, pages={2780-2791}, doi={10.1109/TGRS.2015.2505686}, ISSN={0196-2892}, month={May},} pymap3d-2.7.3/paper/paper.md000066400000000000000000000055701414663415400156570ustar00rootroot00000000000000--- title: 'PyMap3D: 3-D coordinate conversions for terrestrial and geospace environments' tags: authors: - name: Michael Hirsch orcid: 0000-0002-1637-6526 affiliation: "1, 2" affiliations: - name: Boston University ECE Dept. index: 1 - name: SciVision, Inc. index: 2 date: 29 January 2018 bibliography: paper.bib --- # Summary PyMap3D [@pymap3d] is a pure Python coordinate transformation program that converts between geographic coordinate systems and local coordinate systems useful for airborne, space and remote sensing systems. Additional standalone coordinate conversions are provided for Matlab/GNU Octave and Fortran. A subset of PyMap3D functions using syntax compatible with the $1000 Matlab Mapping Toolbox is provided for Matlab and GNU Octave users in the ``matlab/`` directory. A modern Fortran 2018 implementation of many of the PyMap3D routines is provided in the ``fortran/`` directory. The Fortran procedures are "elemental", so they may be used for massively parallel processing of arbitrarily shaped coordinate arrays. For Python, increased performance and accuracy is optionally available for certain functions with AstroPy. Numpy is optional to enable multi-dimensional array inputs, but most of the functions work with Python alone (without Numpy). Other functions that are iterative could possibly be sped up with modules such as Cython or Numba. PyMap3D is targeted for users needing conversions between coordinate systems for observation platforms near Earth's surface, whether underwater, ground-based or space-based platforms. This includes rocket launches, orbiting spacecrafts, UAVs, cameras, radars and many more. By adding ellipsoid parameters, it could be readily be used for other planets as well. The coordinate systems included are: * ECEF (Earth centered, Earth fixed) * ENU (East, North, Up) * NED (North, East, Down) * ECI (Earth Centered Inertial) * Geodetic (Latitude, Longitude, Altitude) * Horizontal Celestial (Alt-Az or Az-El) * Equatorial Celestial (Right Ascension, Declination) Additionally, Vincenty [@vincenty, @veness] geodesic distances and direction are computed. PyMap3D has already seen usage in projects including * [EU ECSEL project 662107 SWARMs](http://swarms.eu/) * Rafael Defense Systems DataHack 2017 * HERA radiotelescope * Mahali (NSF Grant: AGS-1343967) * Solar Eclipse network (NSF Grant: AGS-1743832) * High Speed Auroral Tomography (NSF Grant: AGS-1237376) [@7368896] ## Other Programs Other Python geodesy programs include: * [PyGeodesy](https://github.com/mrJean1/PyGeodesy) MIT license * [PyProj](https://github.com/jswhit/pyproj) ISC license These programs are targeted for geodesy experts, and require additional packages beyond Python that may not be readily accessible to users. Further, these programs do not include all the functions of PyMap3D, and do not have the straightforward function-based API of PyMap3D. # References pymap3d-2.7.3/pyproject.toml000066400000000000000000000016171414663415400160310ustar00rootroot00000000000000[project] name = "pymap3d" version = "2.7.2" description = "pure Python (no prereqs) coordinate conversions, following convention of several popular Matlab routines." readme = "README.md" requires-python = ">=3.7" license = {file = "LICENSE.txt"} keywords = ["geodesy"] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Science/Research', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: GIS' ] authors = [{name = "Michael Hirsch"}] [project.urls] homepage = "https://github.com/geospace-code/pymap3d" [project.optional-dependencies] test = [ "pytest" ] lint = [ "flake8", "flake8-bugbear", "flake8-builtins", "flake8-blind-except", "mypy >= 0.800" ] full = [ "python-dateutil", "numpy >= 1.10.0", "astropy", "xarray" ] [tool.black] line-length = 100 pymap3d-2.7.3/scripts/000077500000000000000000000000001414663415400145775ustar00rootroot00000000000000pymap3d-2.7.3/scripts/benchmark_ecef2geo.py000066400000000000000000000010671414663415400206460ustar00rootroot00000000000000#!/usr/bin/env python3 """ benchmark ecef2geodetic """ import time from pymap3d.ecef import ecef2geodetic import numpy as np import argparse ll0 = (42.0, 82.0) def bench(N: int) -> float: x = np.random.random(N) y = np.random.random(N) z = np.random.random(N) tic = time.monotonic() lat, lon, alt = ecef2geodetic(x, y, z) return time.monotonic() - tic if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("N", type=int) p = p.parse_args() N = p.N print(f"ecef2geodetic: {bench(N):.3f} seconds") pymap3d-2.7.3/scripts/benchmark_vincenty.py000077500000000000000000000024501414663415400210260ustar00rootroot00000000000000#!/usr/bin/env python """ vreckon and vdist are iterative algorithms. How much does PyPy help over Cpython? Hmm, PyPy is slower than Cpython.. $ pypy3 tests/benchmark_vincenty.py 10000 2.1160879135131836 0.06056046485900879 $ python tests/benchmark_vincenty.py 10000 0.3325080871582031 0.02107095718383789 """ import time from pymap3d.vincenty import vreckon, vdist import numpy as np import argparse import subprocess import shutil MATLAB = shutil.which("matlab") ll0 = (42.0, 82.0) def bench_vreckon(N: int) -> float: sr = np.random.random(N) az = np.random.random(N) tic = time.monotonic() a, b = vreckon(ll0[0], ll0[1], sr, az) return time.monotonic() - tic def bench_vdist(N: int) -> float: lat = np.random.random(N) lon = np.random.random(N) tic = time.monotonic() asr, aaz = vdist(ll0[0], ll0[1], lat, lon) return time.monotonic() - tic if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("N", type=int) p = p.parse_args() N = p.N print(f"vreckon: {bench_vreckon(N):.3f}") print(f"vdist: {bench_vdist(N):.3f}") if MATLAB: subprocess.check_call( f'matlab -batch "f = @() distance({ll0[0]}, {ll0[1]}, rand({N},1), rand({N},1)); t = timeit(f); disp(t)"', timeout=45, ) pymap3d-2.7.3/setup.cfg000066400000000000000000000020431414663415400147300ustar00rootroot00000000000000[metadata] name = pymap3d version = 2.7.3 author = Michael Hirsch, Ph.D. author_email = scivision@users.noreply.github.com description = pure Python (no prereqs) coordinate conversions, following convention of several popular Matlab routines. long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/geospace-code/pymap3d keywords = coordinate conversion classifiers = Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Science/Research Operating System :: OS Independent Programming Language :: Python :: 3 Topic :: Scientific/Engineering :: GIS license_files = LICENSE.txt [options] python_requires = >= 3.7 packages = find: zip_safe = False package_dir= =src [options.packages.find] where=src [options.extras_require] tests = pytest lint = flake8 flake8-bugbear flake8-builtins flake8-blind-except mypy >= 0.800 types-python-dateutil types-requests full = python-dateutil numpy >= 1.10.0 astropy xarray testproj = pyproj pymap3d-2.7.3/setup.py000077500000000000000000000001731414663415400146260ustar00rootroot00000000000000#!/usr/bin/env python3 import site import setuptools # PEP517 workaround site.ENABLE_USER_SITE = True setuptools.setup() pymap3d-2.7.3/src/000077500000000000000000000000001414663415400136775ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/000077500000000000000000000000001414663415400152545ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/__init__.py000066400000000000000000000031751414663415400173730ustar00rootroot00000000000000""" PyMap3D provides coordinate transforms and geodesy functions with a similar API to the Matlab Mapping Toolbox, but was of course independently derived. For all functions, the default units are: distance : float METERS angles : float DEGREES time : datetime.datetime UTC time of observation These functions may be used with any planetary body, provided the appropriate reference ellipsoid is defined. The default ellipsoid is WGS-84 deg : bool = True means degrees. False = radians. Most functions accept NumPy arrays of any shape, as well as compatible data types including AstroPy, Pandas and Xarray that have Numpy-like data properties. For clarity, we omit all these types in the docs, and just specify the scalar type. Other languages --------------- Companion packages exist for: * Matlab / GNU Octave: [Matmap3D](https://github.com/geospace-code/matmap3d) * Fortran: [Maptran3D](https://github.com/geospace-code/maptran3d) """ from .aer import ecef2aer, aer2ecef, geodetic2aer, aer2geodetic from .enu import enu2geodetic, geodetic2enu, aer2enu, enu2aer from .ned import ned2ecef, ned2geodetic, geodetic2ned, ecef2nedv, ned2aer, aer2ned, ecef2ned from .ecef import ( geodetic2ecef, ecef2geodetic, eci2geodetic, geodetic2eci, ecef2enuv, enu2ecef, ecef2enu, enu2uvw, uvw2enu, ) from .sidereal import datetime2sidereal, greenwichsrt from .ellipsoid import Ellipsoid from .timeconv import str2dt try: from .azelradec import radec2azel, azel2radec from .eci import eci2ecef, ecef2eci from .aer import eci2aer, aer2eci except ImportError: from .vallado import radec2azel, azel2radec pymap3d-2.7.3/src/pymap3d/aer.py000066400000000000000000000175761414663415400164150ustar00rootroot00000000000000""" transforms involving AER: azimuth, elevation, slant range""" from __future__ import annotations import typing from datetime import datetime from .ecef import ecef2enu, geodetic2ecef, ecef2geodetic, enu2uvw from .enu import geodetic2enu, aer2enu, enu2aer from .ellipsoid import Ellipsoid try: from .eci import eci2ecef, ecef2eci except ImportError: pass if typing.TYPE_CHECKING: from numpy import ndarray __all__ = ["aer2ecef", "ecef2aer", "geodetic2aer", "aer2geodetic", "eci2aer", "aer2eci"] def ecef2aer( x: ndarray, y: ndarray, z: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ compute azimuth, elevation and slant range from an Observer to a Point with ECEF coordinates. ECEF input location is with units of meters Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] """ xEast, yNorth, zUp = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) return enu2aer(xEast, yNorth, zUp, deg=deg) def geodetic2aer( lat: ndarray, lon: ndarray, h: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ gives azimuth, elevation and slant range from an Observer to a Point with geodetic coordinates. Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- az : float azimuth el : float elevation srange : float slant range [meters] """ e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) return enu2aer(e, n, u, deg=deg) def aer2geodetic( az: ndarray, el: ndarray, srange: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ gives geodetic coordinates of a point with az, el, range from an observer at lat0, lon0, h0 Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- In reference ellipsoid system: lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) """ x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell=ell, deg=deg) return ecef2geodetic(x, y, z, ell=ell, deg=deg) def eci2aer( x: ndarray, y: ndarray, z: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, t: datetime, *, deg: bool = True, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ takes Earth Centered Inertial x,y,z ECI coordinates of point and gives az, el, slant range from Observer Parameters ---------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) t : datetime.datetime Observation time deg : bool, optional true: degrees, false: radians use_astropy: bool, optional use Astropy (recommended) Returns ------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] """ try: xecef, yecef, zecef = eci2ecef(x, y, z, t, use_astropy=use_astropy) except NameError: raise ImportError("pip install numpy") return ecef2aer(xecef, yecef, zecef, lat0, lon0, h0, deg=deg) def aer2eci( az: ndarray, el: ndarray, srange: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, t: datetime, ell=None, *, deg: bool = True, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ gives ECI of a point from an observer at az, el, slant range Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) t : datetime.datetime Observation time ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) use_astropy : bool, optional use AstroPy (recommended) Returns ------- Earth Centered Inertial x,y,z x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) """ x, y, z = aer2ecef(az, el, srange, lat0, lon0, h0, ell, deg=deg) try: return ecef2eci(x, y, z, t, use_astropy=use_astropy) except NameError: raise ImportError("pip install numpy") def aer2ecef( az: ndarray, el: ndarray, srange: ndarray, lat0: ndarray, lon0: ndarray, alt0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ converts target azimuth, elevation, range from observer at lat0,lon0,alt0 to ECEF coordinates. Parameters ---------- az : float azimuth to target el : float elevation to target srange : float slant range [meters] lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- ECEF (Earth centered, Earth fixed) x,y,z x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) Notes ------ if srange==NaN, z=NaN """ # Origin of the local system in geocentric coordinates. x0, y0, z0 = geodetic2ecef(lat0, lon0, alt0, ell, deg=deg) # Convert Local Spherical AER to ENU e1, n1, u1 = aer2enu(az, el, srange, deg=deg) # Rotating ENU to ECEF dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) # Origin + offset from origin equals position in ECEF return x0 + dx, y0 + dy, z0 + dz pymap3d-2.7.3/src/pymap3d/azelradec.py000066400000000000000000000057411414663415400175670ustar00rootroot00000000000000""" Azimuth / elevation <==> Right ascension, declination """ from __future__ import annotations from datetime import datetime from .vallado import azel2radec as vazel2radec, radec2azel as vradec2azel from .timeconv import str2dt # astropy can't handle xarray times (yet) try: from astropy.time import Time from astropy import units as u from astropy.coordinates import Angle, SkyCoord, EarthLocation, AltAz, ICRS except ImportError: Time = None __all__ = ["radec2azel", "azel2radec"] def azel2radec( az_deg: float, el_deg: float, lat_deg: float, lon_deg: float, time: datetime, *, use_astropy: bool = True ) -> tuple[float, float]: """ viewing angle (az, el) to sky coordinates (ra, dec) Parameters ---------- az_deg : float azimuth [degrees clockwize from North] el_deg : float elevation [degrees above horizon (neglecting aberration)] lat_deg : float observer latitude [-90, 90] lon_deg : float observer longitude [-180, 180] (degrees) time : datetime.datetime or str time of observation use_astropy : bool, optional default use astropy. Returns ------- ra_deg : float ecliptic right ascension (degress) dec_deg : float ecliptic declination (degrees) """ if use_astropy and Time is not None: obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg) direc = AltAz( location=obs, obstime=Time(str2dt(time)), az=az_deg * u.deg, alt=el_deg * u.deg ) sky = SkyCoord(direc.transform_to(ICRS())) return sky.ra.deg, sky.dec.deg return vazel2radec(az_deg, el_deg, lat_deg, lon_deg, time) def radec2azel( ra_deg: float, dec_deg: float, lat_deg: float, lon_deg: float, time: datetime, *, use_astropy: bool = False ) -> tuple[float, float]: """ sky coordinates (ra, dec) to viewing angle (az, el) Parameters ---------- ra_deg : float ecliptic right ascension (degress) dec_deg : float ecliptic declination (degrees) lat_deg : float observer latitude [-90, 90] lon_deg : float observer longitude [-180, 180] (degrees) time : datetime.datetime or str time of observation use_astropy : bool, optional default use astropy. Returns ------- az_deg : float azimuth [degrees clockwize from North] el_deg : float elevation [degrees above horizon (neglecting aberration)] """ if use_astropy and Time is not None: obs = EarthLocation(lat=lat_deg * u.deg, lon=lon_deg * u.deg) points = SkyCoord(Angle(ra_deg, unit=u.deg), Angle(dec_deg, unit=u.deg), equinox="J2000.0") altaz = points.transform_to(AltAz(location=obs, obstime=Time(str2dt(time)))) return altaz.az.degree, altaz.alt.degree return vradec2azel(ra_deg, dec_deg, lat_deg, lon_deg, time) pymap3d-2.7.3/src/pymap3d/ecef.py000066400000000000000000000275721414663415400165450ustar00rootroot00000000000000""" Transforms involving ECEF: earth-centered, earth-fixed frame """ from __future__ import annotations import typing try: from numpy import ( radians, sin, cos, tan, arctan as atan, hypot, degrees, arctan2 as atan2, sqrt, finfo, where, ) from .eci import eci2ecef, ecef2eci except ImportError: 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 from .utils import sanitize if typing.TYPE_CHECKING: from numpy import ndarray __all__ = [ "geodetic2ecef", "ecef2geodetic", "ecef2enuv", "ecef2enu", "enu2uvw", "uvw2enu", "eci2geodetic", "geodetic2eci", "enu2ecef", ] def geodetic2ecef( lat: ndarray, lon: ndarray, alt: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ point transformation from Geodetic of specified ellipsoid (default WGS-84) to ECEF Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- ECEF (Earth centered, Earth fixed) x,y,z x : float target x ECEF coordinate (meters) y : float target y ECEF coordinate (meters) z : float target z ECEF coordinate (meters) """ lat, ell = sanitize(lat, ell, deg) if deg: lon = radians(lon) # radius of curvature of the prime vertical section N = ell.semimajor_axis ** 2 / sqrt( ell.semimajor_axis ** 2 * cos(lat) ** 2 + ell.semiminor_axis ** 2 * sin(lat) ** 2 ) # Compute cartesian (geocentric) coordinates given (curvilinear) geodetic coordinates. x = (N + alt) * cos(lat) * cos(lon) y = (N + alt) * cos(lat) * sin(lon) z = (N * (ell.semiminor_axis / ell.semimajor_axis) ** 2 + alt) * sin(lat) return x, y, z def ecef2geodetic( x: ndarray, y: ndarray, z: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ convert ECEF (meters) to geodetic coordinates Parameters ---------- x : float target x ECEF coordinate (meters) y : float target y ECEF coordinate (meters) z : float target z ECEF coordinate (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- lat : float target geodetic latitude lon : float target geodetic longitude alt : float target altitude above geodetic ellipsoid (meters) based on: You, Rey-Jer. (2000). Transformation of Cartesian to Geodetic Coordinates without Iterations. Journal of Surveying Engineering. doi: 10.1061/(ASCE)0733-9453 """ if ell is None: ell = Ellipsoid() r = sqrt(x ** 2 + y ** 2 + z ** 2) E = sqrt(ell.semimajor_axis ** 2 - ell.semiminor_axis ** 2) # eqn. 4a u = sqrt(0.5 * (r ** 2 - E ** 2) + 0.5 * sqrt((r ** 2 - E ** 2) ** 2 + 4 * E ** 2 * z ** 2)) Q = hypot(x, y) huE = hypot(u, E) # eqn. 4b try: Beta = atan(huE / u * z / hypot(x, y)) except ZeroDivisionError: if z >= 0: Beta = pi / 2 else: Beta = -pi / 2 # eqn. 13 dBeta = ((ell.semiminor_axis * u - ell.semimajor_axis * huE + E ** 2) * sin(Beta)) / ( ell.semimajor_axis * huE * 1 / cos(Beta) - E ** 2 * cos(Beta) ) Beta += dBeta # eqn. 4c # %% final output lat = atan(ell.semimajor_axis / ell.semiminor_axis * tan(Beta)) try: # patch latitude for float32 precision loss lim_pi2 = pi / 2 - finfo(dBeta.dtype).eps lat = where(Beta >= lim_pi2, pi / 2, lat) lat = where(Beta <= -lim_pi2, -pi / 2, lat) except NameError: pass lon = atan2(y, x) # eqn. 7 cosBeta = cos(Beta) try: # patch altitude for float32 precision loss cosBeta = where(Beta >= lim_pi2, 0, cosBeta) cosBeta = where(Beta <= -lim_pi2, 0, cosBeta) except NameError: pass alt = hypot(z - ell.semiminor_axis * sin(Beta), Q - ell.semimajor_axis * cosBeta) # inside ellipsoid? inside = ( x ** 2 / ell.semimajor_axis ** 2 + y ** 2 / ell.semimajor_axis ** 2 + z ** 2 / ell.semiminor_axis ** 2 < 1 ) try: if inside.any(): # type: ignore # avoid all false assignment bug alt[inside] = -alt[inside] except (TypeError, AttributeError): if inside: alt = -alt if deg: lat = degrees(lat) lon = degrees(lon) return lat, lon, alt def ecef2enuv( u: float, v: float, w: float, lat0: float, lon0: float, deg: bool = True ) -> tuple[float, float, float]: """ VECTOR from observer to target ECEF => ENU Parameters ---------- u : float target x ECEF coordinate (meters) v : float target y ECEF coordinate (meters) w : float target z ECEF coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- uEast : float target east ENU coordinate (meters) vNorth : float target north ENU coordinate (meters) wUp : float target up ENU coordinate (meters) """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lon0) * u + sin(lon0) * v uEast = -sin(lon0) * u + cos(lon0) * v wUp = cos(lat0) * t + sin(lat0) * w vNorth = -sin(lat0) * t + cos(lat0) * w return uEast, vNorth, wUp def ecef2enu( x: ndarray, y: ndarray, z: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ from observer to target, ECEF => ENU Parameters ---------- x : float target x ECEF coordinate (meters) y : float target y ECEF coordinate (meters) z : float target z ECEF coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- East : float target east ENU coordinate (meters) North : float target north ENU coordinate (meters) Up : float target up ENU coordinate (meters) """ x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) return uvw2enu(x - x0, y - y0, z - z0, lat0, lon0, deg=deg) def enu2uvw( east: ndarray, north: ndarray, up: ndarray, lat0: ndarray, lon0: ndarray, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ Parameters ---------- e1 : float target east ENU coordinate (meters) n1 : float target north ENU coordinate (meters) u1 : float target up ENU coordinate (meters) Results ------- u : float v : float w : float """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lat0) * up - sin(lat0) * north w = sin(lat0) * up + cos(lat0) * north u = cos(lon0) * t - sin(lon0) * east v = sin(lon0) * t + cos(lon0) * east return u, v, w def uvw2enu( u: ndarray, v: ndarray, w: ndarray, lat0: ndarray, lon0: ndarray, deg: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ Parameters ---------- u : float v : float w : float Results ------- East : float target east ENU coordinate (meters) North : float target north ENU coordinate (meters) Up : float target up ENU coordinate (meters) """ if deg: lat0 = radians(lat0) lon0 = radians(lon0) t = cos(lon0) * u + sin(lon0) * v East = -sin(lon0) * u + cos(lon0) * v Up = cos(lat0) * t + sin(lat0) * w North = -sin(lat0) * t + cos(lat0) * w return East, North, Up def eci2geodetic( x: ndarray, y: ndarray, z: ndarray, t: datetime, ell: Ellipsoid = None, *, deg: bool = True, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ convert Earth Centered Internal ECI to geodetic coordinates J2000 time Parameters ---------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] t : datetime.datetime, float UTC time ell : Ellipsoid, optional planet ellipsoid model deg : bool, optional if True, degrees. if False, radians use_astropy : bool, optional use AstroPy (recommended) Results ------- lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) eci2geodetic() a.k.a. eci2lla() """ try: xecef, yecef, zecef = eci2ecef(x, y, z, t, use_astropy=use_astropy) except NameError: raise ImportError("pip install numpy") return ecef2geodetic(xecef, yecef, zecef, ell, deg) def geodetic2eci( lat: ndarray, lon: ndarray, alt: ndarray, t: datetime, ell: Ellipsoid = None, *, deg: bool = True, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ convert geodetic coordinates to Earth Centered Internal ECI J2000 frame Parameters ---------- lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) t : datetime.datetime, float UTC time ell : Ellipsoid, optional planet ellipsoid model deg : bool, optional if True, degrees. if False, radians use_astropy: bool, optional use AstroPy (recommended) Results ------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] geodetic2eci() a.k.a lla2eci() """ x, y, z = geodetic2ecef(lat, lon, alt, ell, deg) try: return ecef2eci(x, y, z, t, use_astropy=use_astropy) except NameError: raise ImportError("pip install numpy") def enu2ecef( e1: ndarray, n1: ndarray, u1: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ ENU to ECEF Parameters ---------- e1 : float target east ENU coordinate (meters) n1 : float target north ENU coordinate (meters) u1 : float target up ENU coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- x : float target x ECEF coordinate (meters) y : float target y ECEF coordinate (meters) z : float target z ECEF coordinate (meters) """ x0, y0, z0 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) dx, dy, dz = enu2uvw(e1, n1, u1, lat0, lon0, deg=deg) return x0 + dx, y0 + dy, z0 + dz pymap3d-2.7.3/src/pymap3d/eci.py000066400000000000000000000072371414663415400163770ustar00rootroot00000000000000""" transforms involving ECI earth-centered inertial """ from __future__ import annotations import typing from datetime import datetime from numpy import array, sin, cos, column_stack, empty, atleast_1d try: from astropy.coordinates import GCRS, ITRS, EarthLocation, CartesianRepresentation from astropy.time import Time import astropy.units as u except ImportError: Time = None from .sidereal import greenwichsrt, juliandate if typing.TYPE_CHECKING: from numpy import ndarray __all__ = ["eci2ecef", "ecef2eci"] def eci2ecef( x: ndarray, y: ndarray, z: ndarray, time: datetime, *, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ Observer => Point ECI => ECEF J2000 frame Parameters ---------- x : float ECI x-location [meters] y : float ECI y-location [meters] z : float ECI z-location [meters] time : datetime.datetime time of obsevation (UTC) use_astropy: bool, optional use AstroPy (much more accurate) Results ------- x_ecef : float x ECEF coordinate y_ecef : float y ECEF coordinate z_ecef : float z ECEF coordinate """ if use_astropy and Time is not None: gcrs = GCRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=time) itrs = gcrs.transform_to(ITRS(obstime=time)) x_ecef = itrs.x.value y_ecef = itrs.y.value z_ecef = itrs.z.value else: x = atleast_1d(x) y = atleast_1d(y) z = atleast_1d(z) gst = atleast_1d(greenwichsrt(juliandate(time))) assert x.shape == y.shape == z.shape assert x.size == gst.size eci = column_stack((x.ravel(), y.ravel(), z.ravel())) ecef = empty((x.size, 3)) for i in range(eci.shape[0]): ecef[i, :] = R3(gst[i]) @ eci[i, :].T x_ecef = ecef[:, 0].reshape(x.shape) y_ecef = ecef[:, 1].reshape(y.shape) z_ecef = ecef[:, 2].reshape(z.shape) return x_ecef, y_ecef, z_ecef def ecef2eci( x: ndarray, y: ndarray, z: ndarray, time: datetime, *, use_astropy: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ Point => Point ECEF => ECI J2000 frame Parameters ---------- x : float target x ECEF coordinate y : float target y ECEF coordinate z : float target z ECEF coordinate time : datetime.datetime time of observation use_astropy: bool, optional use AstroPy (much more accurate) Results ------- x_eci : float x ECI coordinate y_eci : float y ECI coordinate z_eci : float z ECI coordinate """ if use_astropy and Time is not None: itrs = ITRS(CartesianRepresentation(x * u.m, y * u.m, z * u.m), obstime=time) gcrs = itrs.transform_to(GCRS(obstime=time)) eci = EarthLocation(*gcrs.cartesian.xyz) x_eci = eci.x.value y_eci = eci.y.value z_eci = eci.z.value else: x = atleast_1d(x) y = atleast_1d(y) z = atleast_1d(z) gst = atleast_1d(greenwichsrt(juliandate(time))) assert x.shape == y.shape == z.shape assert x.size == gst.size ecef = column_stack((x.ravel(), y.ravel(), z.ravel())) eci = empty((x.size, 3)) for i in range(x.size): eci[i, :] = R3(gst[i]).T @ ecef[i, :] x_eci = eci[:, 0].reshape(x.shape) y_eci = eci[:, 1].reshape(y.shape) z_eci = eci[:, 2].reshape(z.shape) return x_eci, y_eci, z_eci def R3(x: float) -> ndarray: """Rotation matrix for ECI""" return array([[cos(x), sin(x), 0], [-sin(x), cos(x), 0], [0, 0, 1]]) pymap3d-2.7.3/src/pymap3d/ellipsoid.py000066400000000000000000000050251414663415400176140ustar00rootroot00000000000000"""Minimal class for planetary ellipsoids""" from math import sqrt class Ellipsoid: """ generate reference ellipsoid parameters https://en.wikibooks.org/wiki/PROJ.4#Spheroid https://nssdc.gsfc.nasa.gov/planetary/factsheet/index.html as everywhere else in this program, distance units are METERS """ def __init__(self, model: str = "wgs84"): """ feel free to suggest additional ellipsoids Parameters ---------- model : str name of ellipsoid """ if model == "wgs84": """https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84""" self.semimajor_axis = 6378137.0 self.semiminor_axis = 6356752.31424518 elif model == "wgs72": self.semimajor_axis = 6378135.0 self.semiminor_axis = 6356750.52001609 elif model == "grs80": """https://en.wikipedia.org/wiki/GRS_80""" self.semimajor_axis = 6378137.0 self.semiminor_axis = 6356752.31414036 elif model == "clarke1866": self.semimajor_axis = 6378206.4 self.semiminor_axis = 6356583.8 elif model == "mars": """ https://tharsis.gsfc.nasa.gov/geodesy.html """ self.semimajor_axis = 3396900.0 self.semiminor_axis = 3376097.80585952 elif model == "moon": self.semimajor_axis = 1738000.0 self.semiminor_axis = self.semimajor_axis elif model == "venus": self.semimajor_axis = 6051000.0 self.semiminor_axis = self.semimajor_axis elif model == "jupiter": self.semimajor_axis = 71492000.0 self.semiminor_axis = 66770054.3475922 elif model == "io": """ https://doi.org/10.1006/icar.1998.5987 """ self.semimajor_axis = 1829.7 self.semiminor_axis = 1815.8 elif model == "pluto": self.semimajor_axis = 1187000.0 self.semiminor_axis = self.semimajor_axis else: raise NotImplementedError( 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 self.thirdflattening = (self.semimajor_axis - self.semiminor_axis) / ( self.semimajor_axis + self.semiminor_axis ) self.eccentricity = sqrt(2 * self.flattening - self.flattening ** 2) pymap3d-2.7.3/src/pymap3d/enu.py000066400000000000000000000114421414663415400164170ustar00rootroot00000000000000""" transforms involving ENU East North Up """ from __future__ import annotations import typing from math import tau try: 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 from .ecef import geodetic2ecef, ecef2geodetic, enu2ecef, uvw2enu from .ellipsoid import Ellipsoid if typing.TYPE_CHECKING: from numpy import ndarray __all__ = ["enu2aer", "aer2enu", "enu2geodetic", "geodetic2enu"] def enu2aer( e: ndarray, n: ndarray, u: ndarray, deg: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ ENU to Azimuth, Elevation, Range Parameters ---------- e : float ENU East coordinate (meters) n : float ENU North coordinate (meters) u : float ENU Up coordinate (meters) deg : bool, optional degrees input/output (False: radians in/out) Results ------- azimuth : float azimuth to rarget elevation : float elevation to target srange : float slant range [meters] """ # 1 millimeter precision for singularity stability try: e[abs(e) < 1e-3] = 0.0 n[abs(n) < 1e-3] = 0.0 u[abs(u) < 1e-3] = 0.0 except TypeError: if abs(e) < 1e-3: e = 0.0 # type: ignore if abs(n) < 1e-3: n = 0.0 # type: ignore if abs(u) < 1e-3: u = 0.0 # type: ignore r = hypot(e, n) slantRange = hypot(r, u) elev = atan2(u, r) az = atan2(e, n) % tau if deg: az = degrees(az) elev = degrees(elev) return az, elev, slantRange def aer2enu( az: ndarray, el: ndarray, srange: float | ndarray, deg: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ Azimuth, Elevation, Slant range to target to East, North, Up Parameters ---------- azimuth : float azimuth clockwise from north (degrees) elevation : float elevation angle above horizon, neglecting aberrations (degrees) srange : float slant range [meters] deg : bool, optional degrees input/output (False: radians in/out) Returns -------- e : float East ENU coordinate (meters) n : float North ENU coordinate (meters) u : float Up ENU coordinate (meters) """ if deg: el = radians(el) az = radians(az) try: if (asarray(srange) < 0).any(): raise ValueError("Slant range [0, Infinity)") except NameError: if srange < 0: raise ValueError("Slant range [0, Infinity)") r = srange * cos(el) return r * sin(az), r * cos(az), srange * sin(el) def enu2geodetic( e: ndarray, n: ndarray, u: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ East, North, Up to target to geodetic coordinates Parameters ---------- e : float East ENU coordinate (meters) n : float North ENU coordinate (meters) u : float Up ENU coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float geodetic latitude lon : float geodetic longitude alt : float altitude above ellipsoid (meters) """ x, y, z = enu2ecef(e, n, u, lat0, lon0, h0, ell, deg=deg) return ecef2geodetic(x, y, z, ell, deg=deg) def geodetic2enu( lat: ndarray, lon: ndarray, h: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- e : float East ENU n : float North ENU u : float Up ENU """ x1, y1, z1 = geodetic2ecef(lat, lon, h, ell, deg=deg) x2, y2, z2 = geodetic2ecef(lat0, lon0, h0, ell, deg=deg) return uvw2enu(x1 - x2, y1 - y2, z1 - z2, lat0, lon0, deg=deg) pymap3d-2.7.3/src/pymap3d/haversine.py000066400000000000000000000060651414663415400176210ustar00rootroot00000000000000""" Compute angular separation in the sky using haversine Note: decimal points on constants made 0 difference in `%timeit` execution time The Meeus algorithm is about 9.5% faster than Astropy/Vicenty on my PC, and gives virtually identical result within double precision arithmetic limitations """ try: from numpy import cos, arcsin, sqrt, radians, degrees except ImportError: from math import cos, sqrt, radians, degrees, asin as arcsin # type: ignore try: from astropy.coordinates.angle_utilities import angular_separation except ImportError: angular_separation = None __all__ = ["anglesep", "anglesep_meeus", "haversine"] def anglesep_meeus(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = True) -> float: """ Parameters ---------- lon0 : float longitude of first point lat0 : float latitude of first point lon1 : float longitude of second point lat1 : float latitude of second point deg : bool, optional degrees input/output (False: radians in/out) Returns ------- sep_rad : float angular separation Meeus p. 109 from "Astronomical Algorithms" by Jean Meeus Ch. 16 p. 111 (16.5) gives angular distance in degrees between two rightAscension,Declination points in the sky. Neglecting atmospheric effects, of course. Meeus haversine method is stable all the way to exactly 0 deg. either the arrays must be the same size, or one of them must be a scalar """ if deg: lon0 = radians(lon0) lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) sep_rad = 2 * arcsin( sqrt(haversine(lat0 - lat1) + cos(lat0) * cos(lat1) * haversine(lon0 - lon1)) ) return degrees(sep_rad) if deg else sep_rad def anglesep(lon0: float, lat0: float, lon1: float, lat1: float, deg: bool = True) -> float: """ Parameters ---------- lon0 : float longitude of first point lat0 : float latitude of first point lon1 : float longitude of second point lat1 : float latitude of second point deg : bool, optional degrees input/output (False: radians in/out) Returns ------- sep_rad : float angular separation For reference, this is from astropy astropy/coordinates/angle_utilities.py Angular separation between two points on a sphere. """ if angular_separation is None: raise ImportError("angledist requires AstroPy. Try angledis_meeus") if deg: lon0 = radians(lon0) lat0 = radians(lat0) lon1 = radians(lon1) lat1 = radians(lat1) sep_rad = angular_separation(lon0, lat0, lon1, lat1) return degrees(sep_rad) if deg else sep_rad def haversine(theta: float) -> float: """ Compute haversine Parameters ---------- theta : float angle (radians) Results ------- htheta : float haversine of `theta` https://en.wikipedia.org/wiki/Haversine Meeus p. 111 """ return (1 - cos(theta)) / 2.0 pymap3d-2.7.3/src/pymap3d/latitude.py000066400000000000000000000410231414663415400174410ustar00rootroot00000000000000"""geodetic transforms to auxilary coordinate systems involving latitude""" from __future__ import annotations import typing from .ellipsoid import Ellipsoid from .utils import sanitize, sign from . import rcurve try: 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, cos, asinh, atanh, exp, pi, sqrt, inf # type: ignore use_numpy = False if typing.TYPE_CHECKING: from numpy import ndarray COS_EPS = 1e-9 # tolerance for angles near abs([90, 270]) __all__ = [ "geodetic2isometric", "isometric2geodetic", "geodetic2rectifying", "rectifying2geodetic", "geodetic2conformal", "conformal2geodetic", "geodetic2parametric", "parametric2geodetic", "geodetic2geocentric", "geocentric2geodetic", "geodetic2authalic", "authalic2geodetic", "geod2geoc", "geoc2geod", ] def geoc2geod( geocentric_lat: ndarray, geocentric_distance: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> float: """ convert geocentric latitude to geodetic latitude, consider mean sea level altitude like Matlab geoc2geod() Parameters ---------- geocentric_lat : float geocentric latitude geocentric_distance: float distance from planet center, meters (NOT altitude above ground!) ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude References ---------- Long, S.A.T. "General-Altitude Transformation between Geocentric and Geodetic Coordinates. Celestial Mechanics (12), 2, p. 225-230 (1975) doi: 10.1007/BF01230214" """ geocentric_lat, ell = sanitize(geocentric_lat, ell, deg) r = geocentric_distance / ell.semimajor_axis geodetic_lat = ( geocentric_lat + (sin(2 * geocentric_lat) / r) * ell.flattening + ((1 / r ** 2 + 1 / (4 * r)) * sin(4 * geocentric_lat)) * ell.flattening ** 2 ) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2geocentric( geodetic_lat: ndarray, alt_m: float, ell: Ellipsoid = None, deg: bool = True ) -> ndarray: """ convert geodetic latitude to geocentric latitude on spheroid surface like Matlab geocentricLatitude() with alt_m = 0 like Matlab geod2geoc() Parameters ---------- geodetic_lat : float geodetic latitude alt_m: float altitude above ellipsoid ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geocentric_lat : float geocentric latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) r = rcurve.transverse(geodetic_lat, ell, deg=False) geocentric_lat = atan((1 - ell.eccentricity ** 2 * (r / (r + alt_m))) * tan(geodetic_lat)) return degrees(geocentric_lat) if deg else geocentric_lat geod2geoc = geodetic2geocentric def geocentric2geodetic( geocentric_lat: ndarray, alt_m: float, ell: Ellipsoid = None, deg: bool = True ) -> ndarray: """ converts from geocentric latitude to geodetic latitude like Matlab geodeticLatitudeFromGeocentric() when alt_m = 0 like Matlab geod2geoc() but with sea level altitude rather than planet center distance Parameters ---------- geocentric_lat : float geocentric latitude alt_m: float altitude above ellipsoid ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geocentric_lat, ell = sanitize(geocentric_lat, ell, deg) r = rcurve.transverse(geocentric_lat, ell, deg=False) geodetic_lat = atan(tan(geocentric_lat) / (1 - ell.eccentricity ** 2 * (r / (r + alt_m)))) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2isometric( geodetic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True ) -> float: """ computes isometric latitude on an ellipsoid like Matlab map.geodesy.IsometricLatitudeConverter.forward() Parameters ---------- lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- isolat : float isometric latiude Notes ----- Isometric latitude is an auxiliary latitude proportional to the spacing of parallels of latitude on an ellipsoidal mercator projection. Based on Deakin, R.E., 2010, 'The Loxodrome on an Ellipsoid', Lecture Notes, School of Mathematical and Geospatial Sciences, RMIT University, January 2010 """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) e = ell.eccentricity isometric_lat = asinh(tan(geodetic_lat)) - e * atanh(e * sin(geodetic_lat)) # same results # a1 = e * sin(geodetic_lat) # y = (1 - a1) / (1 + a1) # a2 = pi / 4 + geodetic_lat / 2 # 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[i] = sign(geodetic_lat[i]) * inf # type: ignore except TypeError: if i: isometric_lat = sign(geodetic_lat) * inf if deg: isometric_lat = degrees(isometric_lat) try: return isometric_lat.squeeze()[()] except AttributeError: return isometric_lat def isometric2geodetic(isometric_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from isometric latitude to geodetic latitude like Matlab map.geodesy.IsometricLatitudeConverter.inverse() Parameters ---------- isometric_lat : float isometric latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ # NOT sanitize for isometric2geo if deg: isometric_lat = radians(isometric_lat) conformal_lat = 2 * atan(exp(isometric_lat)) - (pi / 2) geodetic_lat = conformal2geodetic(conformal_lat, ell, deg=False) return degrees(geodetic_lat) if deg else geodetic_lat def conformal2geodetic(conformal_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from conformal latitude to geodetic latitude like Matlab map.geodesy.ConformalLatitudeConverter.inverse() Parameters ---------- conformal_lat : float conformal latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ conformal_lat, ell = sanitize(conformal_lat, ell, deg) e = ell.eccentricity f1 = e ** 2 / 2 + 5 * e ** 4 / 24 + e ** 6 / 12 + 13 * e ** 8 / 360 f2 = 7 * e ** 4 / 48 + 29 * e ** 6 / 240 + 811 * e ** 8 / 11520 f3 = 7 * e ** 6 / 120 + 81 * e ** 8 / 1120 f4 = 4279 * e ** 8 / 161280 geodetic_lat = ( conformal_lat + f1 * sin(2 * conformal_lat) + f2 * sin(4 * conformal_lat) + f3 * sin(6 * conformal_lat) + f4 * sin(8 * conformal_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat def geodetic2conformal(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from geodetic latitude to conformal latitude like Matlab map.geodesy.ConformalLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- conformal_lat : float conformal latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) e = ell.eccentricity f1 = 1 - e * sin(geodetic_lat) f2 = 1 + e * sin(geodetic_lat) f3 = 1 - sin(geodetic_lat) f4 = 1 + sin(geodetic_lat) # compute conformal latitudes with correction for points at +90 try: conformal_lat = 2 * atan(sqrt((f4 / f3) * ((f1 / f2) ** e))) - (pi / 2) except ZeroDivisionError: conformal_lat = pi / 2 return degrees(conformal_lat) if deg else conformal_lat # %% rectifying def geodetic2rectifying( geodetic_lat: float | ndarray, ell: Ellipsoid = None, deg: bool = True ) -> float: """ converts from geodetic latitude to rectifying latitude like Matlab map.geodesy.RectifyingLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- rectifying_lat : float rectifying latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) n = ell.thirdflattening f1 = 3 * n / 2 - 9 * n ** 3 / 16 f2 = 15 * n ** 2 / 16 - 15 * n ** 4 / 32 f3 = 35 * n ** 3 / 48 f4 = 315 * n ** 4 / 512 rectifying_lat = ( geodetic_lat - f1 * sin(2 * geodetic_lat) + f2 * sin(4 * geodetic_lat) - f3 * sin(6 * geodetic_lat) + f4 * sin(8 * geodetic_lat) ) return degrees(rectifying_lat) if deg else rectifying_lat def rectifying2geodetic( rectifying_lat: ndarray, ell: Ellipsoid = None, deg: bool = True ) -> ndarray: """ converts from rectifying latitude to geodetic latitude like Matlab map.geodesy.RectifyingLatitudeConverter.inverse() Parameters ---------- rectifying_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ rectifying_lat, ell = sanitize(rectifying_lat, ell, deg) n = ell.thirdflattening f1 = 3 * n / 2 - 27 * n ** 3 / 32 f2 = 21 * n ** 2 / 16 - 55 * n ** 4 / 32 f3 = 151 * n ** 3 / 96 f4 = 1097 * n ** 4 / 512 geodetic_lat = ( rectifying_lat + f1 * sin(2 * rectifying_lat) + f2 * sin(4 * rectifying_lat) + f3 * sin(6 * rectifying_lat) + f4 * sin(8 * rectifying_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat # %% authalic def geodetic2authalic(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from geodetic latitude to authalic latitude like Matlab map.geodesy.AuthalicLatitudeConverter.forward() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- authalic_lat : float authalic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) e = ell.eccentricity f1 = e ** 2 / 3 + 31 * e ** 4 / 180 + 59 * e ** 6 / 560 f2 = 17 * e ** 4 / 360 + 61 * e ** 6 / 1260 f3 = 383 * e ** 6 / 45360 authalic_lat = ( geodetic_lat - f1 * sin(2 * geodetic_lat) + f2 * sin(4 * geodetic_lat) - f3 * sin(6 * geodetic_lat) ) return degrees(authalic_lat) if deg else authalic_lat def authalic2geodetic(authalic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from authalic latitude to geodetic latitude like Matlab map.geodesy.AuthalicLatitudeConverter.inverse() Parameters ---------- authalic_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ authalic_lat, ell = sanitize(authalic_lat, ell, deg) e = ell.eccentricity f1 = e ** 2 / 3 + 31 * e ** 4 / 180 + 517 * e ** 6 / 5040 f2 = 23 * e ** 4 / 360 + 251 * e ** 6 / 3780 f3 = 761 * e ** 6 / 45360 geodetic_lat = ( authalic_lat + f1 * sin(2 * authalic_lat) + f2 * sin(4 * authalic_lat) + f3 * sin(6 * authalic_lat) ) return degrees(geodetic_lat) if deg else geodetic_lat # %% parametric def geodetic2parametric(geodetic_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ converts from geodetic latitude to parametric latitude like Matlab parametriclatitude() Parameters ---------- geodetic_lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- parametric_lat : float parametric latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) parametric_lat = atan(sqrt(1 - (ell.eccentricity) ** 2) * tan(geodetic_lat)) return degrees(parametric_lat) if deg else parametric_lat def parametric2geodetic( parametric_lat: ndarray, ell: Ellipsoid = None, deg: bool = True ) -> ndarray: """ converts from parametric latitude to geodetic latitude like Matlab geodeticLatitudeFromParametric() Parameters ---------- parametric_lat : float latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Returns ------- geodetic_lat : float geodetic latiude Notes ----- Equations from J. P. Snyder, "Map Projections - A Working Manual", US Geological Survey Professional Paper 1395, US Government Printing Office, Washington, DC, 1987, pp. 13-18. """ parametric_lat, ell = sanitize(parametric_lat, ell, deg) geodetic_lat = atan(tan(parametric_lat) / sqrt(1 - (ell.eccentricity) ** 2)) return degrees(geodetic_lat) if deg else geodetic_lat pymap3d-2.7.3/src/pymap3d/los.py000066400000000000000000000071611414663415400164300ustar00rootroot00000000000000""" Line of sight intersection of space observer to ellipsoid """ from __future__ import annotations import typing try: from numpy import pi, nan, sqrt, atleast_1d except ImportError: from math import pi, nan, sqrt # 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"] def lookAtSpheroid( lat0: ndarray, lon0: ndarray, h0: ndarray, az: ndarray, tilt: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ Calculates line-of-sight intersection with Earth (or other ellipsoid) surface from above surface / orbit Parameters ---------- lat0 : float observer geodetic latitude lon0 : float observer geodetic longitude h0 : float observer altitude (meters) Must be non-negative since this function doesn't consider terrain az : float azimuth angle of line-of-sight, clockwise from North tilt : float tilt angle of line-of-sight with respect to local vertical (nadir = 0) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float geodetic latitude where the line-of-sight intersects with the Earth ellipsoid lon : float geodetic longitude where the line-of-sight intersects with the Earth ellipsoid d : float slant range (meters) from starting point to intersect point Values will be NaN if the line of sight does not intersect. Algorithm based on https://medium.com/@stephenhartzell/satellite-line-of-sight-intersection-with-earth-d786b4a6a9b6 Stephen Hartzell """ if h0 < 0: raise ValueError("Intersection calculation requires altitude [0, Infinity)") if ell is None: ell = Ellipsoid() try: lat0 = atleast_1d(lat0) lon0 = atleast_1d(lon0) tilt = atleast_1d(tilt) except NameError: pass a = ell.semimajor_axis b = ell.semimajor_axis c = ell.semiminor_axis el = tilt - 90.0 if deg else tilt - pi / 2 e, n, u = aer2enu(az, el, srange=1.0, deg=deg) # fixed 1 km slant range u, v, w = enu2uvw(e, n, u, lat0, lon0, deg=deg) x, y, z = geodetic2ecef(lat0, lon0, h0, deg=deg) value = -(a ** 2) * b ** 2 * w * z - a ** 2 * c ** 2 * v * y - b ** 2 * c ** 2 * u * x radical = ( a ** 2 * b ** 2 * w ** 2 + a ** 2 * c ** 2 * v ** 2 - a ** 2 * v ** 2 * z ** 2 + 2 * a ** 2 * v * w * y * z - a ** 2 * w ** 2 * y ** 2 + b ** 2 * c ** 2 * u ** 2 - b ** 2 * u ** 2 * z ** 2 + 2 * b ** 2 * u * w * x * z - b ** 2 * w ** 2 * x ** 2 - c ** 2 * u ** 2 * y ** 2 + 2 * c ** 2 * u * v * x * y - c ** 2 * v ** 2 * x ** 2 ) magnitude = a ** 2 * b ** 2 * w ** 2 + a ** 2 * c ** 2 * v ** 2 + b ** 2 * c ** 2 * u ** 2 # %% Return nan if radical < 0 or d < 0 because LOS vector does not point towards Earth try: d = (value - a * b * c * sqrt(radical)) / magnitude d[radical < 0] = nan d[d < 0] = nan except ValueError: if radical < 0: d = nan if d < 0: d = nan except TypeError: pass # %% cartesian to ellipsodal lat, lon, _ = ecef2geodetic(x + d * u, y + d * v, z + d * w, deg=deg) try: return lat.squeeze()[()], lon.squeeze()[()], d.squeeze()[()] except AttributeError: return lat, lon, d pymap3d-2.7.3/src/pymap3d/lox.py000066400000000000000000000216231414663415400164340ustar00rootroot00000000000000""" isometric latitude, meridian distance """ from __future__ import annotations import typing try: from numpy import radians, degrees, cos, arctan2 as atan2, tan, array, broadcast_arrays except ImportError: from math import radians, degrees, cos, atan2, tan # type: ignore from math import pi, tau from .ellipsoid import Ellipsoid from .utils import sign from . import rcurve from . import rsphere from .latitude import ( geodetic2rectifying, rectifying2geodetic, geodetic2isometric, geodetic2authalic, authalic2geodetic, ) from .utils import sph2cart, cart2sph if typing.TYPE_CHECKING: from numpy import ndarray __all__ = [ "loxodrome_inverse", "loxodrome_direct", "meridian_arc", "meridian_dist", "departure", "meanm", ] 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. Parameters ---------- lat : float geodetic latitude ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- dist : float distance (meters) """ return meridian_arc(0.0, lat, ell, deg) def meridian_arc(lat1, lat2: ndarray, ell: Ellipsoid = None, deg: bool = True) -> float: """ Computes the ground distance on an ellipsoid between two latitudes. Parameters ---------- lat1, lat2 : float geodetic latitudes ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- dist : float distance (meters) """ if deg: lat1, lat2 = radians(lat1), radians(lat2) rlat1 = geodetic2rectifying(lat1, ell, deg=False) rlat2 = geodetic2rectifying(lat2, ell, deg=False) return rsphere.rectifying(ell) * abs(rlat2 - rlat1) def loxodrome_inverse( lat1: ndarray, lon1: ndarray, lat2: ndarray, lon2: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[float, float]: """ computes the arc length and azimuth of the loxodrome between two points on the surface of the reference ellipsoid like Matlab distance('rh',...) and azimuth('rh',...) Parameters ---------- lat1 : float geodetic latitude of first point lon1 : float geodetic longitude of first point lat2 : float geodetic latitude of second point lon2 : float geodetic longitude of second point ell : Ellipsoid, optional reference ellipsoid (default WGS84) deg : bool, optional degrees input/output (False: radians in/out) Results ------- lox_s : float distance along loxodrome az12 : float azimuth of loxodrome (degrees/radians) Based on Deakin, R.E., 2010, 'The Loxodrome on an Ellipsoid', Lecture Notes, School of Mathematical and Geospatial Sciences, RMIT University, January 2010 [1] Bowring, B.R., 1985, 'The geometry of the loxodrome on the ellipsoid', The Canadian Surveyor, Vol. 39, No. 3, Autumn 1985, pp.223-230. [2] Snyder, J.P., 1987, Map Projections-A Working Manual. U.S. Geological Survey Professional Paper 1395. Washington, DC: U.S. Government Printing Office, pp.15-16 and pp. 44-45. [3] Thomas, P.D., 1952, Conformal Projections in Geodesy and Cartography, Special Publication No. 251, Coast and Geodetic Survey, U.S. Department of Commerce, Washington, DC: U.S. Government Printing Office, p. 66. """ 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 ) dlon = lon2 - lon1 # compute azimuth az12 = atan2(dlon, disolat) aux = abs(cos(az12)) # compute distance along loxodromic curve dist = meridian_arc(lat2, lat1, deg=False, ell=ell) / aux # straight east or west i = aux < COS_EPS try: dist[i] = departure(lon2[i], lon1[i], lat1[i], ell, deg=False) except (AttributeError, TypeError): if i: dist = departure(lon2, lon1, lat1, ell, deg=False) if deg: az12 = degrees(az12) % 360.0 try: return dist.squeeze()[()], az12.squeeze()[()] except AttributeError: return dist, az12 def loxodrome_direct( lat1: float | ndarray, lon1: float | ndarray, rng: float | ndarray, a12: float, ell: Ellipsoid = None, deg: bool = True, ) -> 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 ---------- lat1 : float inital geodetic latitude (degrees) lon1 : float initial geodetic longitude (degrees) rng : float ground distance (meters) a12 : float azimuth (degrees) clockwide from north. ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat2 : float final geodetic latitude (degrees) lon2 : float final geodetic longitude (degrees) """ if deg: lat1, lon1, a12 = radians(lat1), radians(lon1), radians(a12) a12 = a12 % tau try: 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(): # type: ignore raise ValueError("ground distance must be >= 0") except NameError: if abs(lat1) > pi / 2: # type: ignore raise ValueError("-90 <= latitude <= 90") if rng < 0: raise ValueError("ground distance must be >= 0") # compute rectifying sphere latitude and radius reclat = geodetic2rectifying(lat1, ell, deg=False) # compute the new points cosaz = cos(a12) lat2 = reclat + (rng / rsphere.rectifying(ell)) * cosaz # compute rectifying latitude lat2 = rectifying2geodetic(lat2, ell, deg=False) # transform to geodetic latitude 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()[()] # type: ignore except AttributeError: return lat2, lon2 def departure( lon1: ndarray, lon2: ndarray, lat: ndarray, ell: Ellipsoid = None, deg: bool = True ) -> float: """ Computes the distance along a specific parallel between two meridians. like Matlab departure() Parameters ---------- lon1, lon2 : float geodetic longitudes (degrees) lat : float geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- dist: float ground distance (meters) """ if deg: lon1, lon2, lat = radians(lon1), radians(lon2), radians(lat) return rcurve.parallel(lat, ell=ell, deg=False) * ((lon2 - lon1) % pi) def meanm( lat: ndarray, lon: ndarray, ell: Ellipsoid = None, deg: bool = True ) -> tuple[ndarray, ndarray]: """ Computes geographic mean for geographic points on an ellipsoid like Matlab meanm() Parameters ---------- lat : sequence of float geodetic latitude (degrees) lon : sequence of float geodetic longitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- latbar, lonbar: float geographic mean latitude, longitude """ if deg: lat, lon = radians(lat), radians(lon) lat = geodetic2authalic(lat, ell, deg=False) x, y, z = sph2cart(lon, lat, array(1.0)) lonbar, latbar, _ = cart2sph(x.sum(), y.sum(), z.sum()) latbar = authalic2geodetic(latbar, ell, deg=False) if deg: latbar, lonbar = degrees(latbar), degrees(lonbar) return latbar, lonbar pymap3d-2.7.3/src/pymap3d/ned.py000066400000000000000000000151171414663415400164010ustar00rootroot00000000000000""" Transforms involving NED North East Down """ from __future__ import annotations import typing 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 ) -> tuple[ndarray, ndarray, ndarray]: """ converts azimuth, elevation, range to target from observer to North, East, Down Parameters ----------- az : float azimuth elev : float elevation slantRange : float slant range [meters] deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = aer2enu(az, elev, slantRange, deg=deg) return n, e, -u def ned2aer( n: ndarray, e: ndarray, d: ndarray, deg: bool = True ) -> tuple[ndarray, ndarray, ndarray]: """ converts North, East, Down to azimuth, elevation, range Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) deg : bool, optional degrees input/output (False: radians in/out) Results ------- az : float azimuth elev : float elevation slantRange : float slant range [meters] """ return enu2aer(e, n, -d, deg=deg) def ned2geodetic( n: ndarray, e: ndarray, d: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ Converts North, East, Down to target latitude, longitude, altitude Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) """ x, y, z = enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) return ecef2geodetic(x, y, z, ell, deg=deg) def ned2ecef( n: ndarray, e: ndarray, d: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ North, East, Down to target ECEF coordinates Parameters ---------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) """ return enu2ecef(e, n, -d, lat0, lon0, h0, ell, deg=deg) def ecef2ned( x: ndarray, y: ndarray, z: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ Convert ECEF x,y,z to North, East, Down Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = ecef2enu(x, y, z, lat0, lon0, h0, ell, deg=deg) return n, e, -u def geodetic2ned( lat: ndarray, lon: ndarray, h: ndarray, lat0: ndarray, lon0: ndarray, h0: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> tuple[ndarray, ndarray, ndarray]: """ convert latitude, longitude, altitude of target to North, East, Down from observer Parameters ---------- lat : float target geodetic latitude lon : float target geodetic longitude h : float target altitude above geodetic ellipsoid (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude h0 : float observer altitude above geodetic ellipsoid (meters) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Results ------- n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = geodetic2enu(lat, lon, h, lat0, lon0, h0, ell, deg=deg) return n, e, -u def ecef2nedv( x: float, y: float, z: float, lat0: float, lon0: float, deg: bool = True ) -> tuple[float, float, float]: """ for VECTOR between two points Parameters ---------- x : float ECEF x coordinate (meters) y : float ECEF y coordinate (meters) z : float ECEF z coordinate (meters) lat0 : float Observer geodetic latitude lon0 : float Observer geodetic longitude deg : bool, optional degrees input/output (False: radians in/out) Results ------- (Vector) n : float North NED coordinate (meters) e : float East NED coordinate (meters) d : float Down NED coordinate (meters) """ e, n, u = ecef2enuv(x, y, z, lat0, lon0, deg=deg) return n, e, -u pymap3d-2.7.3/src/pymap3d/rcurve.py000066400000000000000000000057241414663415400171440ustar00rootroot00000000000000"""compute radii of curvature for an ellipsoid""" from __future__ import annotations import typing try: from numpy import sin, cos, sqrt except ImportError: 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: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """ compute geocentric radius at geodetic latitude https://en.wikipedia.org/wiki/Earth_radius#Geocentric_radius """ geodetic_lat, ell = sanitize(geodetic_lat, ell, deg) return sqrt( ( (ell.semimajor_axis ** 2 * cos(geodetic_lat)) ** 2 + (ell.semiminor_axis ** 2 * sin(geodetic_lat)) ** 2 ) / ( (ell.semimajor_axis * cos(geodetic_lat)) ** 2 + (ell.semiminor_axis * sin(geodetic_lat)) ** 2 ) ) 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 geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid (meters) """ lat, ell = sanitize(lat, ell, deg) return cos(lat) * transverse(lat, ell, deg=False) def meridian(lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray: """computes the meridional radius of curvature for the ellipsoid like Matlab rcurve('meridian', ...) Parameters ---------- lat : float geodetic latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid """ 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) -> 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 like Matlab rcurve('transverse', ...) Parameters ---------- lat : float latitude (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of ellipsoid (meters) """ lat, ell = sanitize(lat, ell, deg) return ell.semimajor_axis / sqrt(1 - (ell.eccentricity * sin(lat)) ** 2) pymap3d-2.7.3/src/pymap3d/rsphere.py000066400000000000000000000126131414663415400173010ustar00rootroot00000000000000""" compute radii of auxiliary spheres""" from __future__ import annotations import typing try: from numpy import radians, sin, cos, log, sqrt, degrees, asarray except ImportError: from math import radians, sin, cos, log, sqrt, degrees # type: ignore from .ellipsoid import Ellipsoid from . import rcurve from .vincenty import vdist if typing.TYPE_CHECKING: from numpy import ndarray __all__ = [ "eqavol", "authalic", "rectifying", "euler", "curve", "triaxial", "biaxial", ] def eqavol(ell: Ellipsoid = None) -> float: """computes the radius of the sphere with equal volume as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ if ell is None: ell = Ellipsoid() f = ell.flattening return ell.semimajor_axis * (1 - f / 3 - f ** 2 / 9) def authalic(ell: Ellipsoid = None) -> float: """computes the radius of the sphere with equal surface area as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ if ell is None: ell = Ellipsoid() e = ell.eccentricity if e > 0: f1 = ell.semimajor_axis ** 2 / 2 f2 = (1 - e ** 2) / (2 * e) f3 = log((1 + e) / (1 - e)) return sqrt(f1 * (1 + f2 * f3)) else: return ell.semimajor_axis def rectifying(ell: Ellipsoid = None) -> float: """computes the radius of the sphere with equal meridional distances as the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid Returns ------- radius: float radius of sphere """ if ell is None: ell = Ellipsoid() return ((ell.semimajor_axis ** (3 / 2) + ell.semiminor_axis ** (3 / 2)) / 2) ** (2 / 3) def euler( lat1: ndarray, lon1: ndarray, lat2: ndarray, lon2: ndarray, ell: Ellipsoid = None, deg: bool = True, ) -> ndarray: """computes the Euler radii of curvature at the midpoint of the great circle arc defined by the endpoints (lat1,lon1) and (lat2,lon2) Parameters ---------- lat1, lat2 : float geodetic latitudes (degrees) lon1, lon2 : float geodetic longitudes (degrees) ell : Ellipsoid, optional reference ellipsoid deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of sphere """ if not deg: lat1, lon1, lat2, lon2 = degrees(lat1), degrees(lon1), degrees(lat2), degrees(lon2) try: lat1, lat2 = asarray(lat1), asarray(lat2) except NameError: pass latmid = lat1 + (lat2 - lat1) / 2 # compute the midpoint # compute azimuth az = vdist(lat1, lon1, lat2, lon2, ell=ell)[1] # compute meridional and transverse radii of curvature rho = rcurve.meridian(latmid, ell, deg=True) nu = rcurve.transverse(latmid, ell, deg=True) az = radians(az) den = rho * sin(az) ** 2 + nu * cos(az) ** 2 # compute radius of the arc from point 1 to point 2 return rho * nu / den 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 Parameters ---------- lat1 : float geodetic latitudes (degrees) ell : Ellipsoid, optional reference ellipsoid method: str, optional "mean" or "norm" deg : bool, optional degrees input/output (False: radians in/out) Returns ------- radius: float radius of sphere """ if deg: lat = radians(lat) rho = rcurve.meridian(lat, ell, deg=False) nu = rcurve.transverse(lat, ell, deg=False) if method == "mean": return (rho + nu) / 2 elif method == "norm": return sqrt(rho * nu) else: raise ValueError("method must be mean or norm") def triaxial(ell: Ellipsoid = None, method: str = "mean") -> float: """computes triaxial average of the semimajor and semiminor axes of the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid method: str, optional "mean" or "norm" Returns ------- radius: float radius of sphere """ if ell is None: ell = Ellipsoid() if method == "mean": return (2 * ell.semimajor_axis + ell.semiminor_axis) / 3 elif method == "norm": return (ell.semimajor_axis ** 2 * ell.semiminor_axis) ** (1 / 3) else: raise ValueError("method must be mean or norm") def biaxial(ell: Ellipsoid = None, method: str = "mean") -> float: """computes biaxial average of the semimajor and semiminor axes of the ellipsoid Parameters ---------- ell : Ellipsoid, optional reference ellipsoid method: str, optional "mean" or "norm" Returns ------- radius: float radius of sphere """ if ell is None: ell = Ellipsoid() if method == "mean": return (ell.semimajor_axis + ell.semiminor_axis) / 2 elif method == "norm": return sqrt(ell.semimajor_axis * ell.semiminor_axis) else: raise ValueError("method must be mean or norm") pymap3d-2.7.3/src/pymap3d/sidereal.py000066400000000000000000000061351414663415400174230ustar00rootroot00000000000000# Copyright (c) 2014-2018 Michael Hirsch, Ph.D. """ manipulations of sidereal time """ from math import tau from datetime import datetime from .timeconv import str2dt try: from astropy.time import Time import astropy.units as u from astropy.coordinates import Longitude except ImportError: Time = None """ The "usevallado" datetime to julian runs 4 times faster than astropy. However, AstroPy is more accurate. """ __all__ = ["datetime2sidereal", "juliandate", "greenwichsrt"] def datetime2sidereal(time: datetime, lon_radians: float, *, use_astropy: bool = True) -> float: """ Convert ``datetime`` to local sidereal time from D. Vallado "Fundamentals of Astrodynamics and Applications" time : datetime.datetime time to convert lon_radians : float longitude (radians) use_astropy : bool, optional use AstroPy for conversion (False is Vallado) Results ------- tsr : float Local sidereal time """ if isinstance(time, (tuple, list)): return [datetime2sidereal(t, lon_radians) for t in time] if use_astropy and Time is not None: tsr = ( Time(time) .sidereal_time(kind="apparent", longitude=Longitude(lon_radians, unit=u.radian)) .radian ) else: jd = juliandate(str2dt(time)) # %% Greenwich Sidereal time RADIANS gst = greenwichsrt(jd) # %% Algorithm 15 p. 188 rotate GST to LOCAL SIDEREAL TIME tsr = gst + lon_radians return tsr def juliandate(time: datetime) -> float: """ Python datetime to Julian time (days since Jan 1, 4713 BCE) from D.Vallado Fundamentals of Astrodynamics and Applications p.187 and J. Meeus Astronomical Algorithms 1991 Eqn. 7.1 pg. 61 Parameters ---------- time : datetime.datetime time to convert Results ------- jd : float Julian date (days since Jan 1, 4713 BCE) """ if isinstance(time, (tuple, list)): return list(map(juliandate, time)) if time.month < 3: year = time.year - 1 month = time.month + 12 else: year = time.year month = time.month A = int(year / 100.0) B = 2 - A + int(A / 4.0) C = ((time.second / 60.0 + time.minute) / 60.0 + time.hour) / 24.0 return int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + time.day + B - 1524.5 + C def greenwichsrt(Jdate: float) -> float: """ Convert Julian time to sidereal time D. Vallado Ed. 4 Parameters ---------- Jdate: float Julian date (since Jan 1, 4713 BCE) Results ------- tsr : float Sidereal time """ if isinstance(Jdate, (tuple, list)): return list(map(greenwichsrt, Jdate)) # %% Vallado Eq. 3-42 p. 184, Seidelmann 3.311-1 tUT1 = (Jdate - 2451545.0) / 36525.0 # Eqn. 3-47 p. 188 gmst_sec = ( 67310.54841 + (876600 * 3600 + 8640184.812866) * tUT1 + 0.093104 * tUT1 ** 2 - 6.2e-6 * tUT1 ** 3 ) # 1/86400 and %(2*pi) implied by units of radians return gmst_sec * tau / 86400.0 % tau pymap3d-2.7.3/src/pymap3d/tests/000077500000000000000000000000001414663415400164165ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/tests/__init__.py000066400000000000000000000000001414663415400205150ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/tests/test_aer.py000066400000000000000000000050321414663415400205760ustar00rootroot00000000000000from math import radians import pytest from pytest import approx import pymap3d as pm ELL = pm.Ellipsoid() A = ELL.semimajor_axis B = ELL.semiminor_axis @pytest.mark.parametrize( "aer,lla,xyz", [((33, 70, 1000), (42, -82, 200), (660930.2, -4701424.0, 4246579.6))] ) def test_aer2ecef(aer, lla, xyz): x, y, z = pm.aer2ecef(*aer, *lla) assert x == approx(xyz[0]) assert y == approx(xyz[1]) assert z == approx(xyz[2]) assert isinstance(x, float) assert isinstance(y, float) assert isinstance(z, float) raer = (radians(aer[0]), radians(aer[1]), aer[2]) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) assert pm.aer2ecef(*raer, *rlla, deg=False) == approx(xyz) with pytest.raises(ValueError): pm.aer2ecef(aer[0], aer[1], -1, *lla) @pytest.mark.parametrize( "xyz, lla, aer", [ ((A - 1, 0, 0), (0, 0, 0), (0, -90, 1)), ((-A + 1, 0, 0), (0, 180, 0), (0, -90, 1)), ((0, A - 1, 0), (0, 90, 0), (0, -90, 1)), ((0, -A + 1, 0), (0, -90, 0), (0, -90, 1)), ((0, 0, B - 1), (90, 0, 0), (0, -90, 1)), ((0, 0, -B + 1), (-90, 0, 0), (0, -90, 1)), ((660930.19276, -4701424.22296, 4246579.60463), (42, -82, 200), (33, 70, 1000)), ], ) def test_ecef2aer(xyz, lla, aer): assert pm.ecef2aer(*xyz, *lla) == approx(aer) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) raer = (radians(aer[0]), radians(aer[1]), aer[2]) assert pm.ecef2aer(*xyz, *rlla, deg=False) == approx(raer) @pytest.mark.parametrize("aer,enu", [((33, 70, 1000), (186.2775, 286.8422, 939.6926))]) def test_aer_enu(aer, enu): e, n, u = pm.aer2enu(*aer) assert e == approx(enu[0]) assert n == approx(enu[1]) assert u == approx(enu[2]) assert isinstance(e, float) assert isinstance(n, float) assert isinstance(u, float) raer = (radians(aer[0]), radians(aer[1]), aer[2]) assert pm.aer2enu(*raer, deg=False) == approx(enu) with pytest.raises(ValueError): pm.aer2enu(aer[0], aer[1], -1) a, e, r = pm.enu2aer(*enu) assert a == approx(aer[0]) assert e == approx(aer[1]) assert r == approx(aer[2]) assert isinstance(a, float) assert isinstance(e, float) assert isinstance(r, float) assert pm.enu2aer(*enu, deg=False) == approx(raer) @pytest.mark.parametrize("aer,ned", [((33, 70, 1000), (286.8422, 186.2775, -939.6926))]) def test_aer_ned(aer, ned): assert pm.aer2ned(*aer) == approx(ned) with pytest.raises(ValueError): pm.aer2ned(aer[0], aer[1], -1) assert pm.ned2aer(*ned) == approx(aer) pymap3d-2.7.3/src/pymap3d/tests/test_eci.py000066400000000000000000000056461414663415400206020ustar00rootroot00000000000000import pytest from pytest import approx from datetime import datetime import pymap3d as pm @pytest.mark.parametrize("use_astropy", [True, False]) def test_eci2ecef(use_astropy): pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") # this example from Matlab eci2ecef docs eci = [-2981784, 5207055, 3161595] utc = datetime(2019, 1, 4, 12) ecef = pm.eci2ecef(*eci, utc, use_astropy=use_astropy) rel = 0.0001 if use_astropy else 0.02 assert ecef == approx([-5.7627e6, -1.6827e6, 3.1560e6], rel=rel) @pytest.mark.parametrize("use_astropy", [True, False]) def test_ecef2eci(use_astropy): pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") # this example from Matlab ecef2eci docs ecef = [-5762640, -1682738, 3156028] utc = datetime(2019, 1, 4, 12) eci = pm.ecef2eci(*ecef, utc, use_astropy=use_astropy) rel = 0.0001 if use_astropy else 0.01 assert eci == approx([-2.9818e6, 5.2070e6, 3.1616e6], rel=rel) @pytest.mark.parametrize("use_astropy", [True, False]) def test_eci2geodetic(use_astropy): pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") eci = [-2981784, 5207055, 3161595] utc = datetime(2019, 1, 4, 12) lla = pm.eci2geodetic(*eci, utc, use_astropy=use_astropy) rel = 0.0001 if use_astropy else 0.01 assert lla == approx([27.881, -163.722, 408850.65], rel=rel) @pytest.mark.parametrize("use_astropy", [True, False]) def test_geodetic2eci(use_astropy): pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") lla = [27.881, -163.722, 408850.65] utc = datetime(2019, 1, 4, 12) eci = pm.geodetic2eci(*lla, utc, use_astropy=use_astropy) rel = 0.0001 if use_astropy else 0.01 assert eci == approx([-2981784, 5207055, 3161595], rel=rel) @pytest.mark.parametrize("use_astropy", [True, False]) def test_eci2aer(use_astropy): # test coords from Matlab eci2aer pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") t = datetime(1969, 7, 20, 21, 17, 40) eci = [-3.8454e8, -0.5099e8, -0.3255e8] lla = [28.4, -80.5, 2.7] aer = pm.eci2aer(*eci, *lla, t, use_astropy=use_astropy) rel = 0.0001 if use_astropy else 0.01 assert aer == approx([162.55, 55.12, 384013940.9], rel=rel) @pytest.mark.parametrize("use_astropy", [True, False]) def test_aer2eci(use_astropy): # test coords from Matlab aer2eci pytest.importorskip("numpy") if use_astropy: pytest.importorskip("astropy") aer = [162.55, 55.12, 384013940.9] lla = [28.4, -80.5, 2.7] t = datetime(1969, 7, 20, 21, 17, 40) eci = pm.aer2eci(*aer, *lla, t, use_astropy=use_astropy) rel = 0.001 if use_astropy else 0.06 assert eci == approx([-3.8454e8, -0.5099e8, -0.3255e8], rel=rel) with pytest.raises(ValueError): pm.aer2eci(aer[0], aer[1], -1, *lla, t) pymap3d-2.7.3/src/pymap3d/tests/test_elliposid.py000077500000000000000000000023061414663415400220170ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d as pm xyz0 = (660e3, -4700e3, 4247e3) @pytest.mark.parametrize( "model,f", [ ("wgs84", 3.352810664747480e-03), ("wgs72", 3.352779454167505e-03), ("grs80", 3.352810681182319e-03), ("clarke1866", 3.390075303928791e-03), ("moon", 0.0), ], ) def test_reference(model, f): assert pm.Ellipsoid(model).flattening == approx(f) def test_ellipsoid(): assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("wgs84")) == approx( [42.014670535, -82.0064785, 276.9136916] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("grs80")) == approx( [42.014670536, -82.0064785, 276.9137385] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("clarke1866")) == approx( [42.01680003, -82.0064785, 313.9026793] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("mars")) == approx( [42.009428417, -82.006479, 2.981246e6] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("venus")) == approx( [41.8233663, -82.0064785, 3.17878159e5] ) assert pm.ecef2geodetic(*xyz0, ell=pm.Ellipsoid("moon")) == approx( [41.8233663, -82.0064785, 4.630878e6] ) pymap3d-2.7.3/src/pymap3d/tests/test_enu.py000066400000000000000000000031641414663415400206220ustar00rootroot00000000000000from math import radians import pytest from pytest import approx import pymap3d as pm ELL = pm.Ellipsoid() A = ELL.semimajor_axis B = ELL.semiminor_axis @pytest.mark.parametrize("xyz", [(0, A, 50), ([0], [A], [50])], ids=("scalar", "list")) def test_scalar_enu(xyz): """ verify we can handle the wide variety of input data type users might use """ if isinstance(xyz[0], list): pytest.importorskip("numpy") enu = pm.ecef2enu(*xyz, 0, 90, -100) assert pm.enu2ecef(*enu, 0, 90, -100) == approx([0, A, 50]) def test_3d_enu(): np = pytest.importorskip("numpy") xyz = (np.atleast_3d(0), np.atleast_3d(A), np.atleast_3d(50)) enu = pm.ecef2enu(*xyz, 0, 90, -100) assert pm.enu2ecef(*enu, 0, 90, -100) == approx([0, A, 50]) @pytest.mark.parametrize( "enu,lla,xyz", [((0, 0, 0), (0, 0, 0), (A, 0, 0)), ((0, 0, 1000), (0, 0, 0), (A + 1000, 0, 0))] ) def test_enu_ecef(enu, lla, xyz): x, y, z = pm.enu2ecef(*enu, *lla) assert x == approx(xyz[0]) assert y == approx(xyz[1]) assert z == approx(xyz[2]) assert isinstance(x, float) assert isinstance(y, float) assert isinstance(z, float) rlla = (radians(lla[0]), radians(lla[1]), lla[2]) assert pm.enu2ecef(*enu, *rlla, deg=False) == approx(xyz) e, n, u = pm.ecef2enu(*xyz, *lla) assert e == approx(enu[0]) assert n == approx(enu[1]) assert u == approx(enu[2]) assert isinstance(e, float) assert isinstance(n, float) assert isinstance(u, float) e, n, u = pm.ecef2enu(*xyz, *rlla, deg=False) assert e == approx(enu[0]) assert n == approx(enu[1]) assert u == approx(enu[2]) pymap3d-2.7.3/src/pymap3d/tests/test_geodetic.py000077500000000000000000000161401414663415400216170ustar00rootroot00000000000000import pytest from pytest import approx from math import radians, nan, sqrt, isnan import pymap3d as pm lla0 = (42, -82, 200) rlla0 = (radians(lla0[0]), radians(lla0[1]), lla0[2]) xyz0 = (660675.2518247, -4700948.68316, 4245737.66222) ELL = pm.Ellipsoid() A = ELL.semimajor_axis B = ELL.semiminor_axis xyzlla = [ ((A - 1, 0, 0), (0, 0, -1)), ((0, A - 1, 0), (0, 90, -1)), ((0, 0, B - 1), (90, 0, -1)), ((0, 0, B - 1), (89.999999, 0, -1)), ((0, 0, B - 1), (89.99999, 0, -1)), ((0, 0, -B + 1), (-90, 0, -1)), ((0, 0, -B + 1), (-89.999999, 0, -1)), ((0, 0, -B + 1), (-89.99999, 0, -1)), ((-A + 1, 0, 0), (0, 180, -1)), ] llaxyz = [ ((0, 0, -1), (A - 1, 0, 0)), ((0, 90, -1), (0, A - 1, 0)), ((0, -90, -1), (0, -A + 1, 0)), ((90, 0, -1), (0, 0, B - 1)), ((90, 15, -1), (0, 0, B - 1)), ((-90, 0, -1), (0, 0, -B + 1)), ] atol_dist = 1e-6 # 1 micrometer @pytest.mark.parametrize("lla", [(42, -82, 200), ([42], [-82], [200])], ids=("scalar", "list")) def test_scalar_geodetic2ecef(lla): """ verify we can handle the wide variety of input data type users might use """ if isinstance(lla[0], list): pytest.importorskip("numpy") x0, y0, z0 = pm.geodetic2ecef(*lla) assert (x0, y0, z0) == approx(xyz0) def test_3d_geodetic2ecef(): np = pytest.importorskip("numpy") lla = (np.atleast_3d(42), np.atleast_3d(-82), np.atleast_3d(200)) x0, y0, z0 = pm.geodetic2ecef(*lla) assert (x0, y0, z0) == approx(xyz0) def test_scalar_ecef2geodetic(): """ verify we can handle the wide variety of input data type users might use """ lat, lon, alt = pm.ecef2geodetic(xyz0[0], xyz0[1], xyz0[2]) assert [lat, lon, alt] == approx(lla0, rel=1e-4) def test_3d_ecef2geodetic(): np = pytest.importorskip("numpy") xyz = (np.atleast_3d(xyz0[0]), np.atleast_3d(xyz0[1]), np.atleast_3d(xyz0[2])) lat, lon, alt = pm.ecef2geodetic(*xyz) assert [lat, lon, alt] == approx(lla0, rel=1e-4) def test_array_ecef2geodetic(): """ tests ecef2geodetic can handle numpy array data in addition to singular floats """ np = pytest.importorskip("numpy") # test values with no points inside ellipsoid lla0_array = ( np.array([lla0[0], lla0[0]]), np.array([lla0[1], lla0[1]]), np.array([lla0[2], lla0[2]]), ) xyz = pm.geodetic2ecef(*lla0_array) lats, lons, alts = pm.ecef2geodetic(*xyz) assert lats == approx(lla0_array[0]) assert lons == approx(lla0_array[1]) assert alts == approx(lla0_array[2]) # test values with some (but not all) points inside ellipsoid lla0_array_inside = ( np.array([lla0[0], lla0[0]]), np.array([lla0[1], lla0[1]]), np.array([lla0[2], -lla0[2]]), ) xyz = pm.geodetic2ecef(*lla0_array_inside) lats, lons, alts = pm.ecef2geodetic(*xyz) assert lats == approx(lla0_array_inside[0]) assert lons == approx(lla0_array_inside[1]) assert alts == approx(lla0_array_inside[2]) def test_xarray(): xarray = pytest.importorskip("xarray") xr_lla = xarray.DataArray(list(lla0)) xyz = pm.geodetic2ecef(*xr_lla) assert xyz == approx(xyz0) xr_xyz = xarray.DataArray(list(xyz0)) lla = pm.ecef2geodetic(*xr_xyz) assert lla == approx(lla0) def test_pandas(): pandas = pytest.importorskip("pandas") pd_lla = pandas.Series(lla0) xyz = pm.geodetic2ecef(*pd_lla) assert xyz == approx(xyz0) # %% dataframe degenerates to series pd_lla = pandas.DataFrame([[*lla0], [*lla0]], columns=["lat", "lon", "alt_m"]) xyz = pm.geodetic2ecef(pd_lla["lat"], pd_lla["lon"], pd_lla["alt_m"]) assert xyz[0].values == approx(xyz0[0]) assert xyz[1].values == approx(xyz0[1]) assert xyz[2].values == approx(xyz0[2]) def test_ecef(): xyz = pm.geodetic2ecef(*lla0) assert xyz == approx(xyz0) x, y, z = pm.geodetic2ecef(*rlla0, deg=False) assert x == approx(xyz[0]) assert y == approx(xyz[1]) assert z == approx(xyz[2]) with pytest.raises(ValueError): pm.geodetic2ecef(-100, lla0[1], lla0[2]) assert pm.ecef2geodetic(*xyz) == approx(lla0) assert pm.ecef2geodetic(*xyz, deg=False) == approx(rlla0) assert pm.ecef2geodetic((A - 1) / sqrt(2), (A - 1) / sqrt(2), 0) == approx([0, 45, -1]) @pytest.mark.parametrize("lla, xyz", llaxyz) def test_geodetic2ecef(lla, xyz): assert pm.geodetic2ecef(*lla) == approx(xyz, abs=atol_dist) @pytest.mark.parametrize("xyz, lla", xyzlla) def test_ecef2geodetic(xyz, lla): lat, lon, alt = pm.ecef2geodetic(*xyz) assert lat == approx(lla[0]) assert lon == approx(lla[1]) assert alt == approx(lla[2]) @pytest.mark.parametrize( "aer,lla,lla0", [ ((33, 77, 1000), (42.0016981935, -81.99852, 1174.374035), (42, -82, 200)), ((0, 90, 10000), (0, 0, 10000), (0, 0, 0)), ], ) def test_aer_geodetic(aer, lla, lla0): lat1, lon1, alt1 = pm.aer2geodetic(*aer, *lla0) assert lat1 == approx(lla[0]) assert lon1 == approx(lla[1]) assert alt1 == approx(lla[2]) assert isinstance(lat1, float) assert isinstance(lon1, float) assert isinstance(alt1, float) raer = (radians(aer[0]), radians(aer[1]), aer[2]) rlla0 = (radians(lla0[0]), radians(lla0[1]), lla0[2]) assert pm.aer2geodetic(*raer, *rlla0, deg=False) == approx( (radians(lla[0]), radians(lla[1]), lla[2]) ) with pytest.raises(ValueError): pm.aer2geodetic(aer[0], aer[1], -1, *lla0) assert pm.geodetic2aer(*lla, *lla0) == approx(aer, rel=1e-3) assert pm.geodetic2aer(radians(lla[0]), radians(lla[1]), lla[2], *rlla0, deg=False) == approx( raer, rel=1e-3 ) def test_scalar_nan(): a, e, r = pm.geodetic2aer(nan, nan, nan, *lla0) assert isnan(a) and isnan(e) and isnan(r) lat, lon, alt = pm.aer2geodetic(nan, nan, nan, *lla0) assert isnan(lat) and isnan(lon) and isnan(alt) def test_allnan(): np = pytest.importorskip("numpy") anan = np.empty((10, 10)) anan.fill(nan) assert np.isnan(pm.geodetic2aer(anan, anan, anan, *lla0)).all() assert np.isnan(pm.aer2geodetic(anan, anan, anan, *lla0)).all() def test_somenan(): np = pytest.importorskip("numpy") xyz = np.stack((xyz0, (nan, nan, nan))) lat, lon, alt = pm.ecef2geodetic(xyz[:, 0], xyz[:, 1], xyz[:, 2]) assert (lat[0], lon[0], alt[0]) == approx(lla0) @pytest.mark.parametrize("xyz, lla", xyzlla) def test_numpy_ecef2geodetic(xyz, lla): np = pytest.importorskip("numpy") lat, lon, alt = pm.ecef2geodetic( *np.array( [ [xyz], ], dtype=np.float32, ).T ) assert lat[0] == approx(lla[0]) assert lon[0] == approx(lla[1]) assert alt[0] == approx(lla[2]) @pytest.mark.parametrize("lla, xyz", llaxyz) def test_numpy_geodetic2ecef(lla, xyz): np = pytest.importorskip("numpy") x, y, z = pm.geodetic2ecef( *np.array( [ [lla], ], dtype=np.float32, ).T ) atol_dist = 1 # meters assert x[0] == approx(xyz[0], abs=atol_dist) assert y[0] == approx(xyz[1], abs=atol_dist) assert z[0] == approx(xyz[2], abs=atol_dist) pymap3d-2.7.3/src/pymap3d/tests/test_latitude.py000066400000000000000000000153631414663415400216520ustar00rootroot00000000000000import pytest from pytest import approx from math import radians, inf import pymap3d.latitude as latitude import pymap3d.rcurve as rcurve @pytest.mark.parametrize( "geodetic_lat,alt_m,geocentric_lat", [(0, 0, 0), (90, 0, 90), (-90, 0, -90), (45, 0, 44.80757678), (-45, 0, -44.80757678)], ) def test_geodetic_alt_geocentric(geodetic_lat, alt_m, geocentric_lat): assert latitude.geod2geoc(geodetic_lat, alt_m) == approx(geocentric_lat) r = rcurve.geocentric_radius(geodetic_lat) assert latitude.geoc2geod(geocentric_lat, r) == approx(geodetic_lat) assert latitude.geoc2geod(geocentric_lat, 1e5 + r) == approx( latitude.geocentric2geodetic(geocentric_lat, 1e5 + alt_m) ) assert latitude.geod2geoc(geodetic_lat, 1e5 + alt_m) == approx( latitude.geodetic2geocentric(geodetic_lat, 1e5 + alt_m) ) @pytest.mark.parametrize( "geodetic_lat,geocentric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.80757678), (-45, -44.80757678)], ) def test_geodetic_geocentric(geodetic_lat, geocentric_lat): assert latitude.geodetic2geocentric(geodetic_lat, 0) == approx(geocentric_lat) assert latitude.geodetic2geocentric(radians(geodetic_lat), 0, deg=False) == approx( radians(geocentric_lat) ) assert latitude.geocentric2geodetic(geocentric_lat, 0) == approx(geodetic_lat) assert latitude.geocentric2geodetic(radians(geocentric_lat), 0, deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_geocentric(): pytest.importorskip("numpy") assert latitude.geodetic2geocentric([45, 0], 0) == approx([44.80757678, 0]) assert latitude.geocentric2geodetic([44.80757678, 0], 0) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat, isometric_lat", [ (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) assert isolat == approx(isometric_lat) assert isinstance(isolat, float) assert latitude.geodetic2isometric(radians(geodetic_lat), deg=False) == approx( radians(isometric_lat) ) assert latitude.isometric2geodetic(isometric_lat) == approx(geodetic_lat) assert latitude.isometric2geodetic(radians(isometric_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_isometric(): pytest.importorskip("numpy") assert latitude.geodetic2isometric([45, 0]) == approx([50.227466, 0]) assert latitude.isometric2geodetic([50.227466, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,conformal_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.80768406), (-45, -44.80768406), (89, 88.99327)], ) def test_geodetic_conformal(geodetic_lat, conformal_lat): clat = latitude.geodetic2conformal(geodetic_lat) assert clat == approx(conformal_lat) assert isinstance(clat, float) assert latitude.geodetic2conformal(radians(geodetic_lat), deg=False) == approx( radians(conformal_lat) ) assert latitude.conformal2geodetic(conformal_lat) == approx(geodetic_lat) assert latitude.conformal2geodetic(radians(conformal_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_conformal(): pytest.importorskip("numpy") assert latitude.geodetic2conformal([45, 0]) == approx([44.80768406, 0]) assert latitude.conformal2geodetic([44.80768406, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,rectifying_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.855682), (-45, -44.855682)], ) def test_geodetic_rectifying(geodetic_lat, rectifying_lat): assert latitude.geodetic2rectifying(geodetic_lat) == approx(rectifying_lat) assert latitude.geodetic2rectifying(radians(geodetic_lat), deg=False) == approx( radians(rectifying_lat) ) assert latitude.rectifying2geodetic(rectifying_lat) == approx(geodetic_lat) assert latitude.rectifying2geodetic(radians(rectifying_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_rectifying(): pytest.importorskip("numpy") assert latitude.geodetic2rectifying([45, 0]) == approx([44.855682, 0]) assert latitude.rectifying2geodetic([44.855682, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,authalic_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.87170288), (-45, -44.87170288)], ) def test_geodetic_authalic(geodetic_lat, authalic_lat): assert latitude.geodetic2authalic(geodetic_lat) == approx(authalic_lat) assert latitude.geodetic2authalic(radians(geodetic_lat), deg=False) == approx( radians(authalic_lat) ) assert latitude.authalic2geodetic(authalic_lat) == approx(geodetic_lat) assert latitude.authalic2geodetic(radians(authalic_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_authalic(): pytest.importorskip("numpy") assert latitude.geodetic2authalic([45, 0]) == approx([44.87170288, 0]) assert latitude.authalic2geodetic([44.87170288, 0]) == approx([45, 0]) @pytest.mark.parametrize( "geodetic_lat,parametric_lat", [(0, 0), (90, 90), (-90, -90), (45, 44.9037878), (-45, -44.9037878)], ) def test_geodetic_parametric(geodetic_lat, parametric_lat): assert latitude.geodetic2parametric(geodetic_lat) == approx(parametric_lat) assert latitude.geodetic2parametric(radians(geodetic_lat), deg=False) == approx( radians(parametric_lat) ) assert latitude.parametric2geodetic(parametric_lat) == approx(geodetic_lat) assert latitude.parametric2geodetic(radians(parametric_lat), deg=False) == approx( radians(geodetic_lat) ) def test_numpy_geodetic_parametric(): pytest.importorskip("numpy") assert latitude.geodetic2parametric([45, 0]) == approx([44.9037878, 0]) assert latitude.parametric2geodetic([44.9037878, 0]) == approx([45, 0]) @pytest.mark.parametrize("lat", [91, -91]) def test_badvals(lat): # geodetic_isometric is not included on purpose with pytest.raises(ValueError): latitude.geodetic2geocentric(lat, 0) with pytest.raises(ValueError): latitude.geocentric2geodetic(lat, 0) with pytest.raises(ValueError): latitude.geodetic2conformal(lat) with pytest.raises(ValueError): latitude.conformal2geodetic(lat) with pytest.raises(ValueError): latitude.geodetic2rectifying(lat) with pytest.raises(ValueError): latitude.rectifying2geodetic(lat) with pytest.raises(ValueError): latitude.geodetic2authalic(lat) with pytest.raises(ValueError): latitude.authalic2geodetic(lat) with pytest.raises(ValueError): latitude.geodetic2parametric(lat) with pytest.raises(ValueError): latitude.parametric2geodetic(lat) pymap3d-2.7.3/src/pymap3d/tests/test_look_spheroid.py000066400000000000000000000025171414663415400226750ustar00rootroot00000000000000import pytest from pytest import approx from math import nan import pymap3d.los as los @pytest.mark.parametrize( "az,tilt,lat,lon,sr", [ (0, 0, 10, -20, 1e3), (0, 90, nan, nan, nan), (0, 45, 10.009041667, -20, 1.41432515e3), (45, 45, 10.00639336, -19.993549978, 1.414324795e3), (125, 45, 9.99481382, -19.992528, 1.414324671e3), ], ) def test_losint(az, tilt, lat, lon, sr): lla0 = (10, -20, 1e3) lat1, lon1, sr1 = los.lookAtSpheroid(*lla0, az, tilt=tilt) nan_ok = True if tilt == 90 else False assert lat1 == approx(lat, nan_ok=nan_ok) assert lon1 == approx(lon, nan_ok=nan_ok) assert sr1 == approx(sr, nan_ok=nan_ok) assert isinstance(lat1, float) assert isinstance(lon1, float) assert isinstance(sr1, float) def test_badval(): with pytest.raises(ValueError): los.lookAtSpheroid(0, 0, -1, 0, 0) def test_array(): np = pytest.importorskip("numpy") az = [0.0, 10.0, 125.0] tilt = [30.0, 45.0, 90.0] lla0 = (42, -82, 200) lat, lon, sr = los.lookAtSpheroid(*lla0, az, tilt) truth = np.array( [ [42.00103959, lla0[1], 230.9413173], [42.00177328, -81.9995808, 282.84715651], [nan, nan, nan], ] ) assert np.column_stack((lat, lon, sr)) == approx(truth, nan_ok=True) pymap3d-2.7.3/src/pymap3d/tests/test_ned.py000077500000000000000000000027011414663415400206000ustar00rootroot00000000000000from pytest import approx import pymap3d as pm lla0 = (42, -82, 200) aer0 = (33, 70, 1000) ELL = pm.Ellipsoid() A = ELL.semimajor_axis B = ELL.semiminor_axis def test_ecef_ned(): enu = pm.aer2enu(*aer0) ned = (enu[1], enu[0], -enu[2]) xyz = pm.aer2ecef(*aer0, *lla0) n, e, d = pm.ecef2ned(*xyz, *lla0) assert n == approx(ned[0]) assert e == approx(ned[1]) assert d == approx(ned[2]) assert pm.ned2ecef(*ned, *lla0) == approx(xyz) def test_enuv_nedv(): vx, vy, vz = (5, 3, 2) ve, vn, vu = (5.368859646588048, 3.008520763668120, -0.352347711524077) assert pm.ecef2enuv(vx, vy, vz, *lla0[:2]) == approx((ve, vn, vu)) assert pm.ecef2nedv(vx, vy, vz, *lla0[:2]) == approx((vn, ve, -vu)) def test_ned_geodetic(): lat1, lon1, alt1 = pm.aer2geodetic(*aer0, *lla0) enu3 = pm.geodetic2enu(lat1, lon1, alt1, *lla0) ned3 = (enu3[1], enu3[0], -enu3[2]) assert pm.geodetic2ned(lat1, lon1, alt1, *lla0) == approx(ned3) lat, lon, alt = pm.enu2geodetic(*enu3, *lla0) assert lat == approx(lat1) assert lon == approx(lon1) assert alt == approx(alt1) assert isinstance(lat, float) assert isinstance(lon, float) assert isinstance(alt, float) lat, lon, alt = pm.ned2geodetic(*ned3, *lla0) assert lat == approx(lat1) assert lon == approx(lon1) assert alt == approx(alt1) assert isinstance(lat, float) assert isinstance(lon, float) assert isinstance(alt, float) pymap3d-2.7.3/src/pymap3d/tests/test_pyproj.py000077500000000000000000000017071414663415400213620ustar00rootroot00000000000000import pytest from pytest import approx from pymap3d.vincenty import vreckon import pymap3d as pm lla0 = [42, -82, 200] def test_compare_vicenty(): taz, tsr = 38, 3000 pyproj = pytest.importorskip("pyproj") lat2, lon2 = vreckon(10, 20, tsr, taz) p4lon, p4lat, p4a21 = pyproj.Geod(ellps="WGS84").fwd(lon2, lat2, taz, tsr) assert p4lon == approx(lon2, rel=0.0025) assert p4lat == approx(lat2, rel=0.0025) p4az, p4a21, p4sr = pyproj.Geod(ellps="WGS84").inv(20, 10, lon2, lat2) assert (p4az, p4sr) == approx((taz, tsr)) def test_compare_geodetic(): pyproj = pytest.importorskip("pyproj") xyz = pm.geodetic2ecef(*lla0) ecef = pyproj.Proj(proj="geocent", ellps="WGS84", datum="WGS84") lla = pyproj.Proj(proj="latlong", ellps="WGS84", datum="WGS84") assert pyproj.transform(lla, ecef, lla0[1], lla0[0], lla0[2]) == approx(xyz) assert pyproj.transform(ecef, lla, *xyz) == approx((lla0[1], lla0[0], lla0[2])) pymap3d-2.7.3/src/pymap3d/tests/test_rcurve.py000066400000000000000000000021001414663415400213260ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d as pm import pymap3d.rcurve as rcurve ell = pm.Ellipsoid() A = ell.semimajor_axis @pytest.mark.parametrize( "lat,curvature", [(0, A), (90, 0), (-90, 0), (45.0, 4517590.87884893), (-45, 4517590.87884893)] ) def test_rcurve_parallel(lat, curvature): assert rcurve.parallel(lat) == approx(curvature, abs=1e-9) def test_numpy_parallel(): pytest.importorskip("numpy") assert rcurve.parallel([0, 90]) == approx([A, 0], abs=1e-9) @pytest.mark.parametrize( "lat,curvature", [ (0, 6335439.327), (90, 6399593.6258), (-90, 6399593.6258), (45.0, 6367381.8156), (-45, 6367381.8156), ], ) def test_rcurve_meridian(lat, curvature): assert rcurve.meridian(lat) == approx(curvature) def test_numpy_meridian(): pytest.importorskip("numpy") assert rcurve.meridian([0, 90]) == approx([6335439.327, 6399593.6258]) def test_numpy_transverse(): pytest.importorskip("numpy") assert rcurve.transverse([-90, 0, 90]) == approx([6399593.6258, A, 6399593.6258]) pymap3d-2.7.3/src/pymap3d/tests/test_rhumb.py000077500000000000000000000101051414663415400211440ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d.lox as lox @pytest.mark.parametrize("lat,dist", [(0, 0), (90, 10001965.729)]) def test_meridian_dist(lat, dist): assert lox.meridian_dist(lat) == approx(dist) @pytest.mark.parametrize( "lat1,lat2,arclen", [ (0, 0, 0), (0, 90, 10001965.729), (0, -90, 10001965.729), (0, 40, 4429529.03035058), (40, 80, 4455610.84159), ], ) def test_meridian_arc(lat1, lat2, arclen): """ meridianarc(deg2rad(40), deg2rad(80), wgs84Ellipsoid) """ assert lox.meridian_arc(lat1, lat2) == approx(arclen) @pytest.mark.parametrize( "lon1,lon2,lat,dist", [ (0, 0, 0, 0), (0, 90, 0, 10018754.1714), (0, -90, 0, 10018754.1714), (90, 0, 0, 10018754.1714), (-90, 0, 0, 10018754.1714), ], ) def test_departure(lon1, lon2, lat, dist): assert lox.departure(lon1, lon2, lat) == approx(dist) @pytest.mark.parametrize( "lat1,lon1,lat2,lon2,arclen,az", [ (40, -80, 65, -148, 5248666.20853187, 302.0056736), (0, 0, 0, 90, 10018754.17, 90), (0, 0, 0, -90, 10018754.17, 270), (0, 90, 0, 0, 10018754.17, 270), (0, -90, 0, 0, 10018754.17, 90), (1, 0, 0, 0, 110574.4, 180), (-1, 0, 0, 0, 110574.4, 0), ], ) def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az): """ distance('rh', 40, -80, 65, -148, wgs84Ellipsoid) azimuth('rh', 40, -80, 65, -148, wgs84Ellipsoid) """ rhdist, rhaz = lox.loxodrome_inverse(lat1, lon1, lat2, lon2) assert rhdist == approx(arclen) assert rhaz == approx(az) assert isinstance(rhdist, float) assert isinstance(rhaz, float) 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.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), (0, 0, 110574.4, 180, -1, 0), (-1, 0, 110574.4, 0, 0, 0), ], ) 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, 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], [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])]) def test_meanm(lat, lon): pytest.importorskip("numpy") assert lox.meanm(lat, lon) == approx([47.26967, 18.460557]) pymap3d-2.7.3/src/pymap3d/tests/test_rsphere.py000066400000000000000000000024461414663415400215050ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d as pm import pymap3d.rsphere as rsphere import pymap3d.rcurve as rcurve ell = pm.Ellipsoid() A = ell.semimajor_axis def test_geocentric_radius(): assert rcurve.geocentric_radius(0) == approx(ell.semimajor_axis) assert rcurve.geocentric_radius(90) == approx(ell.semiminor_axis) assert rcurve.geocentric_radius(45) == approx(6367490.0) assert rcurve.geocentric_radius(30) == approx(6372824.0) @pytest.mark.parametrize("bad_lat", [-91, 91]) def test_geocentric_radius_badval(bad_lat): with pytest.raises(ValueError): rcurve.geocentric_radius(bad_lat) def test_rsphere_eqavol(): assert rsphere.eqavol() == approx(6371000.8049) def test_rsphere_authalic(): assert rsphere.authalic() == approx(6371007.1809) def test_rsphere_rectifying(): assert rsphere.rectifying() == approx(6367449.1458) def test_rsphere_biaxial(): assert rsphere.biaxial() == approx(6367444.657) def test_rsphere_triaxial(): assert rsphere.triaxial() == approx(6371008.77) def test_rsphere_euler(): assert rsphere.euler(42, 82, 44, 100) == approx(6386606.829131) def test_numpy_rsphere_euler(): pytest.importorskip("numpy") assert rsphere.euler([42, 0], [82, 0], 44, 100) == approx([6386606.829131, 6363111.70923164]) pymap3d-2.7.3/src/pymap3d/tests/test_sidereal.py000077500000000000000000000016051414663415400216240ustar00rootroot00000000000000import pytest from pytest import approx from math import radians from datetime import datetime import pymap3d.sidereal as pmd import pymap3d.haversine as pmh lon = -148 t0 = datetime(2014, 4, 6, 8) sra = 2.90658 ha = 45.482789587392013 @pytest.mark.parametrize( "time, use_astropy", [(t0, False), (t0, True), ([t0], False), ([t0], True)] ) def test_sidereal(time, use_astropy): if use_astropy: pytest.importorskip("astropy") # http://www.jgiesen.de/astro/astroJS/siderealClock/ tsr = pmd.datetime2sidereal(time, radians(lon), use_astropy=use_astropy) if isinstance(tsr, list): tsr = tsr[0] assert tsr == approx(sra, rel=1e-5) def test_anglesep(): pytest.importorskip("astropy") assert pmh.anglesep(35, 23, 84, 20) == approx(ha) def test_anglesep_meeus(): # %% compare with astropy assert pmh.anglesep_meeus(35, 23, 84, 20) == approx(ha) pymap3d-2.7.3/src/pymap3d/tests/test_sky.py000077500000000000000000000022451414663415400206430ustar00rootroot00000000000000import pytest from pytest import approx from datetime import datetime import pymap3d as pm lat, lon = (65, -148) lla0 = (42, -82, 200) azel = (180.1, 80) t0 = datetime(2014, 4, 6, 8) radec = (166.5032081149338, 55.000011165405752) @pytest.mark.parametrize("use_astropy", [True, False]) def test_azel2radec(use_astropy): radec1 = pm.azel2radec(*azel, lat, lon, t0, use_astropy=use_astropy) assert radec1 == approx(radec, rel=0.01) @pytest.mark.parametrize("use_astropy", [True, False]) def test_numpy_azel2radec(use_astropy): pytest.importorskip("numpy") radec1 = pm.azel2radec([180.1, 180.1], [80, 80], lat, lon, t0, use_astropy=use_astropy) assert radec1 == approx(radec, rel=0.01) @pytest.mark.parametrize("use_astropy", [True, False]) def test_radec2azel(use_astropy): azel1 = pm.radec2azel(*radec, lat, lon, t0, use_astropy=use_astropy) assert azel1 == approx(azel, rel=0.01) @pytest.mark.parametrize("use_astropy", [True, False]) def test_numpy_radec2azel(use_astropy): pytest.importorskip("numpy") azel1 = pm.radec2azel([166.503208, 166.503208], [55, 55], lat, lon, t0, use_astropy=use_astropy) assert azel1 == approx(azel, rel=0.01) pymap3d-2.7.3/src/pymap3d/tests/test_time.py000077500000000000000000000024761414663415400210010ustar00rootroot00000000000000import pytest from pytest import approx from datetime import datetime from pymap3d.timeconv import str2dt import pymap3d.sidereal as pms t0 = datetime(2014, 4, 6, 8) def test_juliantime(): assert pms.juliandate(t0) == approx(2.456753833333e6) def test_types(): np = pytest.importorskip("numpy") assert str2dt(t0) == t0 # passthrough assert str2dt("2014-04-06T08:00:00") == t0 ti = [str2dt("2014-04-06T08:00:00"), str2dt("2014-04-06T08:01:02")] to = [t0, datetime(2014, 4, 6, 8, 1, 2)] assert ti == to # even though ti is numpy array of datetime and to is list of datetime t1 = [t0, t0] assert (np.asarray(str2dt(t1)) == t0).all() def test_datetime64(): np = pytest.importorskip("numpy") t1 = np.datetime64(t0) assert str2dt(t1) == t0 t1 = np.array([np.datetime64(t0), np.datetime64(t0)]) assert (str2dt(t1) == t0).all() def test_xarray_time(): xarray = pytest.importorskip("xarray") t = {"time": t0} ds = xarray.Dataset(t) assert str2dt(ds["time"]) == t0 t2 = {"time": [t0, t0]} ds = xarray.Dataset(t2) assert (str2dt(ds["time"]) == t0).all() def test_pandas_time(): pandas = pytest.importorskip("pandas") t = pandas.Series(t0) assert (str2dt(t) == t0).all() t = pandas.Series([t0, t0]) assert (str2dt(t) == t0).all() pymap3d-2.7.3/src/pymap3d/tests/test_vincenty.py000077500000000000000000000003641414663415400216740ustar00rootroot00000000000000from pytest import approx import pymap3d.vincenty as vincenty def test_track2(): lats, lons = vincenty.track2(40, 80, 65, -148, npts=3) assert lats == approx([40, 69.633139886, 65]) assert lons == approx([80, 113.06849104, -148]) pymap3d-2.7.3/src/pymap3d/tests/test_vincenty_dist.py000066400000000000000000000026341414663415400227160ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d.vincenty as vincenty @pytest.mark.parametrize( "lat,lon,lat1,lon1,srange,az", [ (0, 0, 0, 0, 0, 0), (0, 0, 0, 90, 1.001875e7, 90), (0, 0, 0, -90, 1.001875e7, 270), (0, 0, 0, 180, 2.00375e7, 90), (0, 0, 0, -180, 2.00375e7, 90), (0, 0, 0, 4, 445277.96, 90), (0, 0, 0, 5, 556597.45, 90), (0, 0, 0, 6, 667916.94, 90), (0, 0, 0, -6, 667916.94, 270), (0, 0, 0, 7, 779236.44, 90), (1e-16, 1e-16, 1e-16, 1, 111319.49, 90), (90, 0, 0, 0, 1.00019657e7, 180), (90, 0, -90, 0, 2.000393145e7, 180), ], ) def test_unit(lat, lon, lat1, lon1, srange, az): dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) assert dist == approx(srange, rel=0.005) assert az1 == approx(az) assert isinstance(dist, float) assert isinstance(az1, float) def test_vector(): pytest.importorskip("numpy") asr, aaz = vincenty.vdist(10, 20, [10.02137267, 10.01917819], [20.0168471, 20.0193493]) assert 3e3 == approx(asr) assert aaz == approx([38, 45]) @pytest.mark.parametrize("lat,lon,slantrange,az", [(10, 20, 3e3, 38), (0, 0, 0, 0)]) def test_identity(lat, lon, slantrange, az): lat1, lon1 = vincenty.vreckon(lat, lon, slantrange, az) dist, az1 = vincenty.vdist(lat, lon, lat1, lon1) assert dist == approx(slantrange) assert az1 == approx(az) pymap3d-2.7.3/src/pymap3d/tests/test_vincenty_vreckon.py000066400000000000000000000023421414663415400234160ustar00rootroot00000000000000import pytest from pytest import approx import pymap3d.vincenty as vincenty ll0 = [10, 20] lat2 = [10.02137267, 10.01917819] lon2 = [20.0168471, 20.0193493] az2 = [218.00292856, 225.00336316] sr1 = [3e3, 1e3] az1 = [38, 45] lat3 = (10.02137267, 10.00639286) lon3 = (20.0168471, 20.00644951) az3 = (218.00292856, 225.0011203) @pytest.mark.parametrize( "lat,lon,srange,az,lato,lono", [ (0, 0, 0, 0, 0, 0), (0, 0, 1.001875e7, 90, 0, 90), (0, 0, 1.001875e7, 270, 0, 270), (0, 0, 1.001875e7, -90, 0, 270), (0, 0, 2.00375e7, 90, 0, 180), (0, 0, 2.00375e7, 270, 0, 180), (0, 0, 2.00375e7, -90, 0, 180), ], ) def test_unit(lat, lon, srange, az, lato, lono): lat1, lon1 = vincenty.vreckon(lat, lon, srange, az) assert lat1 == approx(lato) assert isinstance(lat1, float) assert lon1 == approx(lono, rel=0.001) assert isinstance(lon1, float) def test_az_vector(): pytest.importorskip("numpy") a, b = vincenty.vreckon(*ll0, sr1[0], az1) assert a == approx(lat2) assert b == approx(lon2) def test_both_vector(): pytest.importorskip("numpy") a, b = vincenty.vreckon(10, 20, sr1, az1) assert a == approx(lat3) assert b == approx(lon3) pymap3d-2.7.3/src/pymap3d/timeconv.py000066400000000000000000000026561414663415400174630ustar00rootroot00000000000000# Copyright (c) 2014-2018 Michael Hirsch, Ph.D. """ convert strings to datetime """ from datetime import datetime try: from dateutil.parser import parse except ImportError: parse = None # type: ignore try: import numpy as np except ImportError: np = None # type: ignore __all__ = ["str2dt"] def str2dt(time: datetime) -> datetime: """ Converts times in string or list of strings to datetime(s) Parameters ---------- time : str or datetime.datetime or numpy.datetime64 Results ------- t : datetime.datetime """ if isinstance(time, datetime): return time elif isinstance(time, str): if parse is None: raise TypeError("expected datetime") return parse(time) elif np is not None and isinstance(time, np.datetime64): return time.astype(datetime) else: # some sort of iterable try: if isinstance(time[0], datetime): return time elif np is not None and isinstance(time[0], np.datetime64): return time.astype(datetime) elif isinstance(time[0], str): if parse is None: raise TypeError("expected datetime") return [parse(t) for t in time] except (IndexError, TypeError): pass # last resort--assume pandas/xarray return time.values.astype("datetime64[us]").astype(datetime) pymap3d-2.7.3/src/pymap3d/utils.py000066400000000000000000000040001414663415400167600ustar00rootroot00000000000000"""Utility functions all assume radians""" from __future__ import annotations import typing from math import pi from .ellipsoid import Ellipsoid try: from numpy import hypot, cos, sin, arctan2 as atan2, radians, asarray, sign except ImportError: from math import atan2, hypot, cos, sin, radians # type: ignore def sign(x: float) -> float: # type: ignore """signum function""" if x < 0: y = -1.0 elif x > 0: y = 1.0 else: y = 0.0 return y if typing.TYPE_CHECKING: from numpy import ndarray __all__ = ["cart2pol", "pol2cart", "cart2sph", "sph2cart", "sign"] def cart2pol(x: float, y: float) -> tuple[float, float]: """Transform Cartesian to polar coordinates""" return atan2(y, x), hypot(x, y) def pol2cart(theta: float, rho: float) -> tuple[float, float]: """Transform polar to Cartesian coordinates""" return rho * cos(theta), rho * sin(theta) 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) el = atan2(z, hxy) az = atan2(y, x) return az, el, r 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) y = rcos_theta * sin(az) z = r * sin(el) return x, y, z def sanitize( lat: float | ndarray, ell: typing.Optional[Ellipsoid], deg: bool ) -> tuple[float | ndarray, Ellipsoid]: if ell is None: ell = Ellipsoid() try: lat = asarray(lat) except NameError: pass if deg: lat = radians(lat) try: if (abs(lat) > pi / 2).any(): # type: ignore raise ValueError("-pi/2 <= latitude <= pi/2") except AttributeError: if abs(lat) > pi / 2: # type: ignore raise ValueError("-pi/2 <= latitude <= pi/2") return lat, ell pymap3d-2.7.3/src/pymap3d/vallado.py000066400000000000000000000070341414663415400172540ustar00rootroot00000000000000""" converts right ascension, declination to azimuth, elevation and vice versa. Normally do this via AstroPy. These functions are fallbacks for those wihtout AstroPy. Michael Hirsch implementation of algorithms from D. Vallado """ from __future__ import annotations from datetime import datetime try: from numpy import sin, cos, degrees, radians, arcsin as asin, arctan2 as atan2 except ImportError: from math import sin, cos, degrees, radians, asin, atan2 # type: ignore from .sidereal import datetime2sidereal __all__ = ["azel2radec", "radec2azel"] def azel2radec( az_deg: float, el_deg: float, lat_deg: float, lon_deg: float, time: datetime, *, use_astropy: bool = True ) -> tuple[float, float]: """ converts azimuth, elevation to right ascension, declination Parameters ---------- az_deg : float azimuth (clockwise) to point [degrees] el_deg : float elevation above horizon to point [degrees] lat_deg : float observer WGS84 latitude [degrees] lon_deg : float observer WGS84 longitude [degrees] time : datetime.datetime time of observation use_astropy : bool, optional use AstroPy Results ------- ra_deg : float right ascension to target [degrees] dec_deg : float declination of target [degrees] from D.Vallado Fundamentals of Astrodynamics and Applications p.258-259 """ if abs(lat_deg) > 90: raise ValueError("-90 <= lat <= 90") az = radians(az_deg) el = radians(el_deg) lat = radians(lat_deg) lon = radians(lon_deg) # %% Vallado "algorithm 28" p 268 dec = asin(sin(el) * sin(lat) + cos(el) * cos(lat) * cos(az)) lha = atan2( -(sin(az) * cos(el)) / cos(dec), (sin(el) - sin(lat) * sin(dec)) / (cos(dec) * cos(lat)) ) lst = datetime2sidereal(time, lon, use_astropy=use_astropy) # lon, ra in RADIANS """ by definition right ascension [0, 360) degrees """ return degrees(lst - lha) % 360, degrees(dec) def radec2azel( ra_deg: float, dec_deg: float, lat_deg: float, lon_deg: float, time: datetime, *, use_astropy: bool = True ) -> tuple[float, float]: """ converts right ascension, declination to azimuth, elevation Parameters ---------- ra_deg : float right ascension to target [degrees] dec_deg : float declination to target [degrees] lat_deg : float observer WGS84 latitude [degrees] lon_deg : float observer WGS84 longitude [degrees] time : datetime.datetime time of observation use_astropy : bool, optional use Astropy if available Results ------- az_deg : float azimuth clockwise from north to point [degrees] el_deg : float elevation above horizon to point [degrees] from D. Vallado "Fundamentals of Astrodynamics and Applications " 4th Edition Ch. 4.4 pg. 266-268 """ if abs(lat_deg) > 90: raise ValueError("-90 <= lat <= 90") ra = radians(ra_deg) dec = radians(dec_deg) lat = radians(lat_deg) lon = radians(lon_deg) lst = datetime2sidereal(time, lon, use_astropy=use_astropy) # RADIANS # %% Eq. 4-11 p. 267 LOCAL HOUR ANGLE lha = lst - ra # %% #Eq. 4-12 p. 267 el = asin(sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(lha)) # %% combine Eq. 4-13 and 4-14 p. 268 az = atan2( -sin(lha) * cos(dec) / cos(el), (sin(dec) - sin(el) * sin(lat)) / (cos(el) * cos(lat)) ) return degrees(az) % 360.0, degrees(el) pymap3d-2.7.3/src/pymap3d/vdist/000077500000000000000000000000001414663415400164055ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/vdist/__init__.py000066400000000000000000000000001414663415400205040ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/vdist/__main__.py000066400000000000000000000010121414663415400204710ustar00rootroot00000000000000import argparse from .. import vincenty p = argparse.ArgumentParser(description="Vincenty algorithms") p.add_argument("lat1", help="latitude1 WGS-84 [degrees]", type=float) p.add_argument("lon1", help="longitude1 WGS-84 [degrees]", type=float) p.add_argument("lat2", help="latitude2 WGS-84 [degrees]", type=float) p.add_argument("lon2", help="longitude2 WGS-84 [degrees]", type=float) P = p.parse_args() dist_m = vincenty.vdist(P.lat1, P.lon1, P.lat2, P.lon2) print("{:.3f} meters {:.3f} deg azimuth".format(*dist_m)) pymap3d-2.7.3/src/pymap3d/vincenty.py000066400000000000000000000446001414663415400174710ustar00rootroot00000000000000""" Vincenty's methods for computing ground distance and reckoning """ from __future__ import annotations import typing import logging from math import nan, pi from copy import copy try: from numpy import ( atleast_1d, sqrt, tan, sin, cos, isnan, arctan as atan, arctan2 as atan2, arcsin as asin, radians, degrees, ) except ImportError: from math import sqrt, tan, sin, cos, isnan, atan, atan2, asin, radians, degrees # type: ignore from .ellipsoid import Ellipsoid from .utils import sign if typing.TYPE_CHECKING: from numpy import ndarray __all__ = ["vdist", "vreckon", "track2"] def vdist( Lat1: float | ndarray, Lon1: float | ndarray, Lat2: float | ndarray, Lon2: float | ndarray, ell: Ellipsoid = None, ) -> tuple[ndarray, ndarray]: """ Using the reference ellipsoid, compute the distance between two points within a few millimeters of accuracy, compute forward azimuth, and compute backward azimuth, all using a vectorized version of Vincenty's algorithm: Example: dist_m, azimuth_deg = vdist(lat1, lon1, lat2, lon2, ell) Parameters ---------- Lat1 : float Geodetic latitude of first point (degrees) Lon1 : float Geodetic longitude of first point (degrees) Lat2 : float Geodetic latitude of second point (degrees) Lon2 : float Geodetic longitude of second point (degrees) ell : Ellipsoid, optional reference ellipsoid Results ------- dist_m : float distance (meters) az : float azimuth (degrees) clockwise from first point to second point (forward) Original algorithm source: T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations", Survey Review, vol. 23, no. 176, April 1975, pp 88-93. Available at: http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf Notes: 1. lat1,lon1,lat2,lon2 can be any (identical) size/shape. Outputs will have the same size and shape. 2. Error correcting code, convergence failure traps, antipodal corrections, polar error corrections, WGS84 ellipsoid parameters, testing, and comments: Michael Kleder, 2004. 3. Azimuth implementation (including quadrant abiguity resolution) and code vectorization, Michael Kleder, Sep 2005. 4. Vectorization is convergence sensitive; that is, quantities which have already converged to within tolerance are not recomputed during subsequent iterations (while other quantities are still converging). 5. Vincenty describes his distance algorithm as precise to within 0.01 millimeters, subject to the ellipsoidal model. 6. For distance calculations, essentially antipodal points are treated as exactly antipodal, potentially reducing accuracy slightly. 7. Distance failures for points exactly at the poles are eliminated by moving the points by 0.6 millimeters. 8. The Vincenty distance algorithm was transcribed verbatim by Peter Cederholm, August 12, 2003. It was modified and translated to English by Michael Kleder. Mr. Cederholm's website is http://www.plan.aau.dk/~pce/ 9. Distances agree with the Mapping Toolbox, version 2.2 (R14SP3) with a max relative difference of about 5e-9, except when the two points are nearly antipodal, and except when one point is near the equator and the two longitudes are nearly 180 degrees apart. This function (vdist) is more accurate in such cases. For example, note this difference (as of this writing): ```python vdist(0.2,305,15,125) ``` > 18322827.0131551 ```python distance(0.2,305,15,125,[6378137 0.08181919]) ``` > 0 10. Azimuths FROM the north pole (either forward starting at the north pole or backward when ending at the north pole) are set to 180 degrees by convention. Azimuths FROM the south pole are set to 0 degrees by convention. 11. Azimuths agree with the Mapping Toolbox, version 2.2 (R14SP3) to within about a hundred-thousandth of a degree, except when traversing to or from a pole, where the convention for this function is described in (10), and except in the cases noted above in (9). 12. No warranties; use at your own risk. """ if ell is None: ell = Ellipsoid() # %% Input check: try: Lat1 = atleast_1d(Lat1) Lon1 = atleast_1d(Lon1) Lat2 = atleast_1d(Lat2) Lon2 = atleast_1d(Lon2) 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): # type: ignore raise ValueError("Input latitudes must be in [-90, 90] degrees.") # %% Supply WGS84 earth ellipsoid axis lengths in meters: a = ell.semimajor_axis b = ell.semiminor_axis f = ell.flattening # %% convert inputs in degrees to radians: lat1 = radians(Lat1) lon1 = radians(Lon1) lat2 = radians(Lat2) lon2 = radians(Lon2) # %% correct for errors at exact poles by adjusting 0.6 millimeters: try: i = abs(pi / 2 - abs(lat1)) < 1e-10 lat1[i] = sign(lat1[i]) * (pi / 2 - 1e-10) i = abs(pi / 2 - abs(lat2)) < 1e-10 lat2[i] = sign(lat2[i]) * (pi / 2 - 1e-10) except TypeError: if abs(pi / 2 - abs(lat1)) < 1e-10: lat1 = sign(lat1) * (pi / 2 - 1e-10) if abs(pi / 2 - abs(lat2)) < 1e-10: lat2 = sign(lat2) * (pi / 2 - 1e-10) U1 = atan((1 - f) * tan(lat1)) U2 = atan((1 - f) * tan(lat2)) lon1 = lon1 % (2 * pi) lon2 = lon2 % (2 * pi) L = abs(lon2 - lon1) try: L[L > pi] = 2 * pi - L[L > pi] except TypeError: if L > pi: L = 2 * pi - L # type: ignore lamb = copy(L) # NOTE: program will fail without copy! itercount = 0 warninggiven = False notdone = True while notdone: # force at least one execution itercount += 1 if itercount > 50: if not warninggiven: logging.warning("Essentially antipodal points--precision may be reduced slightly.") lamb = pi # type: ignore break lambdaold = copy(lamb) sinsigma = sqrt( (cos(U2) * sin(lamb)) ** 2 + (cos(U1) * sin(U2) - sin(U1) * cos(U2) * cos(lamb)) ** 2 ) cossigma = sin(U1) * sin(U2) + cos(U1) * cos(U2) * cos(lamb) # eliminate rare imaginary portions at limit of numerical precision: sinsigma = sinsigma.real cossigma = cossigma.real sigma = atan2(sinsigma, cossigma) try: sinAlpha = cos(U1) * cos(U2) * sin(lamb) / sin(sigma) alpha = asin(sinAlpha) alpha[isnan(sinAlpha)] = 0 alpha[(sinAlpha > 1) | (abs(sinAlpha - 1) < 1e-16)] = pi / 2 except (ZeroDivisionError, TypeError, ValueError): try: sinAlpha = cos(U1) * cos(U2) * sin(lamb) / sin(sigma) except ZeroDivisionError: sinAlpha = 0.0 if isnan(sinAlpha): alpha = 0.0 elif sinAlpha > 1 or abs(sinAlpha - 1) < 1e-16: alpha = pi / 2 else: alpha = asin(sinAlpha) cos2sigmam = cos(sigma) - 2 * sin(U1) * sin(U2) / cos(alpha) ** 2 C = f / 16 * cos(alpha) ** 2 * (4 + f * (4 - 3 * cos(alpha) ** 2)) lamb = L + (1 - C) * f * sin(alpha) * ( sigma + C * sin(sigma) * (cos2sigmam + C * cos(sigma) * (-1 + 2.0 * cos2sigmam ** 2)) ) # print(f'then, lambda(21752) = {lamb[21752],20}) # correct for convergence failure for essentially antipodal points try: i = (lamb > pi).any() # type: ignore except AttributeError: i = lamb > pi if i: logging.warning( "Essentially antipodal points encountered. Precision may be reduced slightly." ) warninggiven = True lambdaold = pi # type: ignore lamb = pi # type: ignore try: notdone = (abs(lamb - lambdaold) > 1e-12).any() # type: ignore except AttributeError: notdone = abs(lamb - lambdaold) > 1e-12 u2 = cos(alpha) ** 2 * (a ** 2 - b ** 2) / b ** 2 A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2))) B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2))) deltasigma = ( B * sin(sigma) * ( cos2sigmam + B / 4 * ( cos(sigma) * (-1 + 2 * cos2sigmam ** 2) - B / 6 * cos2sigmam * (-3 + 4 * sin(sigma) ** 2) * (-3 + 4 * cos2sigmam ** 2) ) ) ) dist_m = b * A * (sigma - deltasigma) # %% From point #1 to point #2 # correct sign of lambda for azimuth calcs: lamb = abs(lamb) try: i = sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0 lamb[i] = -lamb[i] except TypeError: if sign(sin(lon2 - lon1)) * sign(sin(lamb)) < 0: lamb = -lamb numer = cos(U2) * sin(lamb) denom = cos(U1) * sin(U2) - sin(U1) * cos(U2) * cos(lamb) a12 = atan2(numer, denom) a12 %= 2 * pi az = degrees(a12) try: return dist_m.squeeze()[()], az.squeeze()[()] except AttributeError: return dist_m, az def vreckon( Lat1: float | ndarray, Lon1: float | ndarray, Rng: ndarray, Azim: ndarray, ell: Ellipsoid = None ) -> tuple[ndarray, ndarray]: """ This is the Vincenty "forward" solution. Computes points at a specified azimuth and range in an ellipsoidal earth. Using the reference ellipsoid, travel a given distance along a given azimuth starting at a given initial point, and return the endpoint within a few millimeters of accuracy, using Vincenty's algorithm. Example: lat2, lon2 = vreckon(lat1, lon1, ground_range_m, azimuth_deg) Parameters ---------- Lat1 : float inital geodetic latitude (degrees) Lon1 : float initial geodetic longitude (degrees) Rng : float ground distance (meters) Azim : float intial azimuth (degrees) clockwide from north. ell : Ellipsoid, optional reference ellipsoid Results ------- Lat2 : float final geodetic latitude (degrees) Lon2 : float final geodetic longitude (degrees) Original algorithm: T. Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with Application of Nested Equations", Survey Review, vol. 23, no. 176, April 1975, pp 88-93. http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf Notes: 1. The Vincenty reckoning algorithm was transcribed verbatim into JavaScript by Chris Veness. It was modified and translated to Matlab by Michael Kleder. Mr. Veness's website is: http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html 2. Error correcting code, polar error corrections, WGS84 ellipsoid parameters, testing, and comments by Michael Kleder. 3. By convention, when starting at a pole, the longitude of the initial point (otherwise meaningless) determines the longitude line along which to traverse, and hence the longitude of the final point. 4. The convention noted in (3) above creates a discrepancy with VDIST when the the intial or final point is at a pole. In the VDIST function, when traversing from a pole, the azimuth is 0 when heading away from the south pole and 180 when heading away from the north pole. In contrast, this VRECKON function uses the azimuth as noted in (3) above when traversing away form a pole. 5. In testing, where the traversal subtends no more than 178 degrees, this function correctly inverts the VDIST function to within 0.2 millimeters of distance, 5e-10 degrees of forward azimuth, and 5e-10 degrees of reverse azimuth. Precision reduces as test points approach antipodal because the precision of VDIST is reduced for nearly antipodal points. (A warning is given by VDIST.) 6. Tested but no warranty. Use at your own risk. 7. Ver 1.0, Michael Kleder, November 2007. Ver 2.0, Joaquim Luis, September 2008 Added ellipsoid and vectorized whenever possible. Also, lon2 is always converted to the [-180 180] interval. Joaquim Luis """ try: Lat1 = atleast_1d(Lat1) Lon1 = atleast_1d(Lon1) Rng = atleast_1d(Rng) Azim = atleast_1d(Azim) if (abs(Lat1) > 90.0).any(): raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.") if (Rng < 0.0).any(): raise ValueError("Ground distance must be positive") except NameError: if abs(Lat1) > 90.0: # type: ignore raise ValueError("Input lat. must be between -90 and 90 deg., inclusive.") if Rng < 0.0: raise ValueError("Ground distance must be positive") if ell is not None: a = ell.semimajor_axis b = ell.semiminor_axis f = ell.flattening else: # Supply WGS84 earth ellipsoid axis lengths in meters: a = 6378137 # semimajor axis b = 6356752.31424518 # WGS84 earth flattening coefficient definition f = (a - b) / a lat1 = radians(Lat1) # intial latitude in radians lon1 = radians(Lon1) # intial longitude in radians # correct for errors at exact poles by adjusting 0.6 millimeters: try: i = abs(pi / 2 - abs(lat1)) < 1e-10 lat1[i] = sign(lat1[i]) * (pi / 2 - (1e-10)) except TypeError: if abs(pi / 2 - abs(lat1)) < 1e-10: lat1 = sign(lat1) * (pi / 2 - (1e-10)) alpha1 = radians(Azim) # inital azimuth in radians sinAlpha1 = sin(alpha1) cosAlpha1 = cos(alpha1) tanU1 = (1 - f) * tan(lat1) cosU1 = 1 / sqrt(1 + tanU1 ** 2) sinU1 = tanU1 * cosU1 sigma1 = atan2(tanU1, cosAlpha1) sinAlpha = cosU1 * sinAlpha1 cosSqAlpha = 1 - sinAlpha * sinAlpha uSq = cosSqAlpha * (a ** 2 - b ** 2) / b ** 2 A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) sigma = Rng / (b * A) sigmaP = 2 * pi sinSigma = nan cosSigma = nan cos2SigmaM = nan try: i = (abs(sigma - sigmaP) > 1e-12).any() except AttributeError: i = abs(sigma - sigmaP) > 1e-12 while i: cos2SigmaM = cos(2 * sigma1 + sigma) sinSigma = sin(sigma) cosSigma = cos(sigma) deltaSigma = ( B * sinSigma * ( cos2SigmaM + B / 4 * ( cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM) ) ) ) sigmaP = sigma sigma = Rng / (b * A) + deltaSigma try: i = (abs(sigma - sigmaP) > 1e-12).any() except AttributeError: i = abs(sigma - sigmaP) > 1e-12 tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1 lat2 = atan2( sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1 - f) * sqrt(sinAlpha * sinAlpha + tmp ** 2), ) lamb = atan2(sinSigma * sinAlpha1, cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1) C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)) L = lamb - ( f * (1 - C) * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))) ) lon2 = degrees(lon1 + L) # Truncates angles into the [-pi pi] range # if lon2 > pi: # lon2 = pi*((absolute(lon2)/pi) - # 2*ceil(((absolute(lon2)/pi)-1)/2)) * sign(lon2) lon2 = lon2 % 360 # follow [0, 360) convention try: return degrees(lat2).squeeze()[()], lon2.squeeze()[()] except AttributeError: return degrees(lat2), lon2 def track2( lat1: ndarray, lon1: ndarray, lat2: ndarray, lon2: ndarray, ell: Ellipsoid = None, npts: int = 100, deg: bool = True, ) -> tuple[list[ndarray], list[ndarray]]: """ computes great circle tracks starting at the point lat1, lon1 and ending at lat2, lon2 Parameters ---------- Lat1 : float Geodetic latitude of first point (degrees) Lon1 : float Geodetic longitude of first point (degrees) Lat2 : float Geodetic latitude of second point (degrees) Lon2 : float Geodetic longitude of second point (degrees) ell : Ellipsoid, optional reference ellipsoid npts : int, optional number of points (default is 100) deg : bool, optional degrees input/output (False: radians in/out) Results ------- lats : list of float latitudes of points along track lons : list of float longitudes of points along track Based on code posted to the GMT mailing list in Dec 1999 by Jim Levens and by Jeff Whitaker """ if ell is None: ell = Ellipsoid() if npts < 2: raise ValueError("npts must be greater than 1") if npts == 2: return [lat1, lat2], [lon1, lon2] if deg: rlat1 = radians(lat1) rlon1 = radians(lon1) rlat2 = radians(lat2) rlon2 = radians(lon2) else: rlat1, rlon1, rlat2, rlon2 = lat1, lon1, lat2, lon2 gcarclen = 2.0 * asin( sqrt( (sin((rlat1 - rlat2) / 2)) ** 2 + cos(rlat1) * cos(rlat2) * (sin((rlon1 - rlon2) / 2)) ** 2 ) ) # check to see if points are antipodal (if so, route is undefined). if abs(gcarclen - pi) < 1e-12: raise ValueError( "cannot compute intermediate points on a great circle whose endpoints are antipodal" ) distance, azimuth = vdist(lat1, lon1, lat2, lon2) incdist = distance / (npts - 1) latpt = lat1 lonpt = lon1 lons = [lonpt] lats = [latpt] for _ in range(npts - 2): latptnew, lonptnew = vreckon(latpt, lonpt, incdist, azimuth) azimuth = vdist(latptnew, lonptnew, lat2, lon2, ell=ell)[1] lats.append(latptnew) lons.append(lonptnew) latpt = latptnew lonpt = lonptnew lons.append(lon2) lats.append(lat2) if not deg: lats = list(map(radians, lats)) lons = list(map(radians, lons)) return lats, lons pymap3d-2.7.3/src/pymap3d/vreckon/000077500000000000000000000000001414663415400167235ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/vreckon/__init__.py000066400000000000000000000000001414663415400210220ustar00rootroot00000000000000pymap3d-2.7.3/src/pymap3d/vreckon/__main__.py000066400000000000000000000011601414663415400210130ustar00rootroot00000000000000from .. import vincenty import argparse p = argparse.ArgumentParser( description="Given starting latitude, longitude: find final lat,lon for distance and azimuth" ) p.add_argument("lat", help="latitude WGS-84 [degrees]", type=float) p.add_argument("lon", help="longitude WGS-84 [degrees]", type=float) p.add_argument("range", help="range from start point [meters]", type=float) p.add_argument("azimuth", help="clockwise from north: azimuth to start [degrees]", type=float) P = p.parse_args() lat2, lon2 = vincenty.vreckon(P.lat, P.lon, P.range, P.azimuth) print(f"lat, lon = ({lat2.item():.4f}, {lon2.item():.4f})")