├── .gitignore ├── CMakeLists.txt ├── HOWTO-RELEASE.txt ├── LICENSE ├── README.md ├── distrib-Python └── 00README.md ├── doc ├── CMakeLists.txt ├── code.rst ├── conf.py ├── examples.rst ├── geodesics.rst ├── index.rst ├── interface.rst └── package.rst ├── geographiclib ├── CMakeLists.txt ├── __init__.py.in ├── accumulator.py ├── constants.py ├── geodesic.py ├── geodesiccapability.py ├── geodesicline.py ├── geomath.py ├── polygonarea.py └── test │ ├── __init__.py │ ├── test_geodesic.py │ └── test_sign.py ├── pyproject.toml └── setup.cfg.in /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | /MANIFEST 4 | BUILD* 5 | distrib-Python 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project (GeographicLib-Python NONE) 2 | 3 | # The project name for python 4 | set (PYTHON_PROJECT_NAME geographiclib) 5 | 6 | # Version information 7 | set (PROJECT_VERSION_MAJOR 2) 8 | set (PROJECT_VERSION_MINOR 0) 9 | set (PROJECT_VERSION_PATCH 0) 10 | set (PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") 11 | if (PROJECT_VERSION_PATCH GREATER 0) 12 | set (PROJECT_VERSION "${PROJECT_VERSION}.${PROJECT_VERSION_PATCH}") 13 | endif () 14 | # Follow Python conventions on the version suffix: {a,b,rc}N, see 15 | # https://www.python.org/dev/peps/pep-0440 16 | set (PROJECT_VERSION_SUFFIX "") 17 | set (PROJECT_FULLVERSION "${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX}") 18 | 19 | set (RELEASE_DATE "2022-04-23") 20 | 21 | cmake_minimum_required (VERSION 3.13.0) 22 | 23 | # Set a default build type for single-configuration cmake generators if 24 | # no build type is set. 25 | if (NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE) 26 | set (CMAKE_BUILD_TYPE Release) 27 | endif () 28 | 29 | configure_file (setup.cfg.in setup.cfg @ONLY) 30 | set (EXTRA_FILES README.md LICENSE pyproject.toml) 31 | foreach (f ${EXTRA_FILES}) 32 | configure_file (${f} ${f} COPYONLY) 33 | endforeach () 34 | 35 | # Minimum version of Python supported in 3.7 (for math.remainder). Next 36 | # update will be to 3.10 (to catch the fix in math.hypot). 37 | set (Python_VERSION_NUMBER 3.7 CACHE STRING "The minimum version of Python") 38 | find_package (Python ${Python_VERSION_NUMBER} COMPONENTS Interpreter) 39 | message (STATUS 40 | "Python found: ${Python_EXECUTABLE}, Version: ${Python_VERSION}") 41 | 42 | # Set source files here since they are used in multiple places 43 | set (PYTHON_FILES 44 | accumulator.py constants.py geodesiccapability.py 45 | geodesicline.py geodesic.py geomath.py polygonarea.py) 46 | set (TEST_FILES 47 | test/__init__.py test/test_geodesic.py test/test_sign.py) 48 | 49 | add_subdirectory (geographiclib) 50 | 51 | enable_testing () 52 | # Run the test suite 53 | add_test (NAME testsuite 54 | COMMAND ${Python_EXECUTABLE} -m unittest -v geographiclib.test.test_geodesic) 55 | add_test (NAME testsign 56 | COMMAND ${Python_EXECUTABLE} -m unittest -v geographiclib.test.test_sign) 57 | 58 | # For Python documentation 59 | find_program (SPHINX sphinx-build) 60 | if (SPHINX) 61 | add_subdirectory (doc) 62 | endif () 63 | 64 | set (ALLDEPENDS) 65 | foreach (f setup.cfg ${EXTRA_FILES}) 66 | set (ALLDEPENDS 67 | ${ALLDEPENDS} "${CMAKE_CURRENT_BINARY_DIR}/${f}") 68 | endforeach () 69 | foreach (f __init__.py ${PYTHON_FILES} ${TEST_FILES}) 70 | set (ALLDEPENDS 71 | ${ALLDEPENDS} "${CMAKE_CURRENT_BINARY_DIR}/geographiclib/${f}") 72 | endforeach () 73 | 74 | set (SDIST dist/${PYTHON_PROJECT_NAME}-${PROJECT_FULLVERSION}.tar.gz) 75 | set (WHEEL dist/${PYTHON_PROJECT_NAME}-${PROJECT_FULLVERSION}-py3-none-any.whl) 76 | add_custom_command (OUTPUT ${SDIST} ${WHEEL} 77 | COMMAND ${Python_EXECUTABLE} -m build 78 | DEPENDS ${ALLDEPENDS} 79 | COMMENT "Creating packages") 80 | 81 | add_custom_target (package ALL DEPENDS ${SDIST} ${WHEEL}) 82 | 83 | add_custom_target (deploy-package 84 | COMMAND ${Python_EXECUTABLE} -m twine upload ${SDIST} ${WHEEL} 85 | DEPENDS package) 86 | 87 | set (INSTALL_COMMAND 88 | "-m pip install -q --prefix ${CMAKE_INSTALL_PREFIX} ${WHEEL}") 89 | install (CODE 90 | "message (STATUS \"python ${INSTALL_COMMAND}\")") 91 | install (CODE 92 | "execute_process (COMMAND ${Python_EXECUTABLE} ${INSTALL_COMMAND})") 93 | 94 | if (IS_DIRECTORY ${PROJECT_SOURCE_DIR}/.git AND NOT WIN32) 95 | add_custom_target (checktrailingspace 96 | COMMAND git ls-files | xargs grep ' $$' || true 97 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 98 | COMMENT "Looking for trailing spaces") 99 | add_custom_target (checktabs 100 | COMMAND git ls-files | xargs grep '\t' || true 101 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 102 | COMMENT "Looking for tabs") 103 | add_custom_target (checkblanklines 104 | COMMAND git ls-files | 105 | while read f\; do tr 'X\\n' 'YX' < $$f | 106 | egrep '\(^X|XXX|XX$$|[^X]$$\)' > /dev/null && echo $$f\; done || true 107 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 108 | COMMENT "Looking for extra blank lines") 109 | 110 | add_custom_target (sanitize) 111 | add_dependencies (sanitize checktrailingspace checktabs checkblanklines) 112 | 113 | find_program (RSYNC rsync) 114 | if (IS_DIRECTORY ${PROJECT_SOURCE_DIR}/distrib-Python AND RSYNC) 115 | set (USER karney) 116 | set (DATAROOT $ENV{HOME}/web/geographiclib-files/distrib-Python) 117 | set (DOCROOT $ENV{HOME}/web/geographiclib-web/htdocs/Python) 118 | set (FRSDEPLOY ${USER}@frs.sourceforge.net:/home/frs/project/geographiclib) 119 | set (WEBDEPLOY ${USER},geographiclib@web.sourceforge.net:./htdocs) 120 | 121 | add_custom_target (stage-dist 122 | COMMAND ${CMAKE_COMMAND} -E copy_if_different 123 | ${SDIST} ${WHEEL} ${PROJECT_SOURCE_DIR}/distrib-Python/ 124 | COMMAND ${RSYNC} --delete -av --exclude '*~' 125 | ${PROJECT_SOURCE_DIR}/distrib-Python/ ${DATAROOT}/) 126 | add_dependencies (stage-dist package) 127 | 128 | if (SPHINX) 129 | add_custom_target (stage-doc 130 | COMMAND ${RSYNC} --delete -a 131 | doc/html/ ${DOCROOT}/${PROJECT_VERSION}/) 132 | add_dependencies (stage-doc doc) 133 | endif () 134 | 135 | add_custom_target (deploy-dist 136 | COMMAND ${RSYNC} --delete -av ${DATAROOT} ${FRSDEPLOY}/) 137 | add_custom_target (deploy-doc 138 | COMMAND ${RSYNC} --delete -av -e ssh ${DOCROOT} ${WEBDEPLOY}/) 139 | endif () 140 | endif () 141 | -------------------------------------------------------------------------------- /HOWTO-RELEASE.txt: -------------------------------------------------------------------------------- 1 | cmake cache values 2 | Python_VERSION_NUMBER 3.7 3 | 4 | cmake targets: 5 | 6 | all = package + doc 7 | lint 8 | sanitize 9 | install = python -m pip install -q --prefix ${CMAKE_INSTALL_PREFIX} ${WHEEL} 10 | installs to lib/python3.10/site-packages/geographiclib 11 | 12 | stage-doc 13 | stage-dist 14 | deploy-doc -- upload to sourceforge 15 | deploy-dist -- upload packages to sourceforge 16 | deploy-package -- upload packages to pypi 17 | 18 | https://pypi.org/project/geographiclib 19 | 20 | Version update checks 21 | CMakeLists.txt verson + date, change log + date in doc/package.rst 22 | remove devel versions from distrib-Python 23 | make all lint sanitize 24 | make stage-{doc,dist} 25 | make deploy-{doc,dist} 26 | make deploy-package 27 | 28 | https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-your-project-to-pypi 29 | 30 | python release -- authentication via ~/.pypirc 31 | PIP maintenance 32 | python -m pip install --upgrade build 33 | python -m pip install --upgrade twine 34 | 35 | build invoked by make package 36 | twine invoked by make deploy-package 37 | 38 | After release check conda-forge build 39 | conda-forge maintainers: 40 | https://github.com/ocefpaf 41 | https://github.com/QuLogic 42 | https://github.com/beckermr 43 | 44 | Fedora maintainers: 45 | Rich Mattes 46 | 47 | Debian + Ubuntu maintainers: 48 | Bas Couwenberg 49 | Francesco Paolo Lovergine 50 | 51 | conda-forge: https://anaconda.org/conda-forge/geographiclib 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT). 2 | 3 | Copyright (c) 2012-2022, Charles Karney 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, copy, 9 | modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python implementation of the geodesic routines in GeographicLib 2 | 3 | This is a library to solve geodesic problems on an ellipsoid model of 4 | the earth. 5 | 6 | Licensed under the MIT/X11 License; see 7 | [LICENSE.txt](https://geographiclib.sourceforge.io/LICENSE.txt). 8 | 9 | The algorithms are documented in 10 | 11 | * C. F. F. Karney, 12 | [Algorithms for geodesics](https://doi.org/10.1007/s00190-012-0578-z), 13 | J. Geodesy **87**(1), 43–55 (2013); 14 | [Addenda](https://geographiclib.sourceforge.io/geod-addenda.html). 15 | 16 | The documentation for this package is in 17 | https://geographiclib.sourceforge.io/Python/doc 18 | -------------------------------------------------------------------------------- /distrib-Python/00README.md: -------------------------------------------------------------------------------- 1 | # Python implementation of the geodesic routines in GeographicLib 2 | 3 | Source packages are `.tar.gz` files. Built package are the `.whl` 4 | files. To install the dist run 5 | ```bash 6 | python -m pip install [WHEEL-FILE] 7 | ``` 8 | 9 | This package is also hosted on 10 | [PyPI](https://pypi.python.org/pypi/geographiclib). 11 | 12 | The algorithms are documented in 13 | 14 | * C. F. F. Karney, 15 | [Algorithms for geodesics](https://doi.org/10.1007/s00190-012-0578-z), 16 | J. Geodesy **87**(1), 43–55 (2013); 17 | [Addenda](https://geographiclib.sourceforge.io/geod-addenda.html). 18 | 19 | Other links: 20 | 21 | * Library documentation: https://geographiclib.sourceforge.io/Python/doc 22 | * GIT repository: https://github.com/geographiclib/geographiclib-python 23 | * Source distribution: 24 | https://sourceforge.net/projects/geographiclib/files/distrib-Python 25 | * GeographicLib: https://geographiclib.sourceforge.io 26 | * Author: Charles Karney, 27 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set (FILES conf.py 2 | index.rst package.rst geodesics.rst interface.rst code.rst examples.rst) 3 | set (SOURCES) 4 | foreach (f ${SOURCES}) 5 | set (SOURCES ${SOURCES} "../geographiclib/${f}") 6 | endforeach () 7 | 8 | configure_file (conf.py . COPYONLY) 9 | 10 | add_custom_target (doc ALL 11 | DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/html/index.html) 12 | add_custom_command (OUTPUT 13 | ${CMAKE_CURRENT_BINARY_DIR}/html/index.html 14 | DEPENDS ${FILES} ${SOURCES} 15 | COMMAND ${SPHINX} -q -b html -d doctree -c . ${CMAKE_CURRENT_SOURCE_DIR} html 16 | COMMENT "Generating documentation tree") 17 | -------------------------------------------------------------------------------- /doc/code.rst: -------------------------------------------------------------------------------- 1 | GeographicLib API 2 | ================= 3 | 4 | geographiclib 5 | ------------- 6 | .. automodule:: geographiclib 7 | :members: __version_info__, __version__ 8 | 9 | geographiclib.geodesic 10 | ---------------------- 11 | .. automodule:: geographiclib.geodesic 12 | :members: 13 | 14 | .. data:: Geodesic.WGS84 15 | :annotation: = Instantiation for the WGS84 ellipsoid 16 | 17 | geographiclib.geodesicline 18 | -------------------------- 19 | .. automodule:: geographiclib.geodesicline 20 | :members: 21 | 22 | geographiclib.polygonarea 23 | ------------------------- 24 | .. automodule:: geographiclib.polygonarea 25 | :members: 26 | 27 | geographiclib.constants 28 | ----------------------- 29 | .. automodule:: geographiclib.constants 30 | :members: 31 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # geographiclib documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Oct 24 17:14:00 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import sphinx 18 | sys.path.insert(0, os.path.abspath('..')) 19 | import geographiclib 20 | import geographiclib.geodesic 21 | import geographiclib.geodesicline 22 | import geographiclib.polygonarea 23 | import geographiclib.geodesiccapability 24 | import geographiclib.geomath 25 | import geographiclib.constants 26 | import geographiclib.accumulator 27 | 28 | # If extensions (or modules to document with autodoc) are in another directory, 29 | # add these directories to sys.path here. If the directory is relative to the 30 | # documentation root, use os.path.abspath to make it absolute, like shown here. 31 | #sys.path.insert(0, os.path.abspath('.')) 32 | 33 | # -- General configuration ------------------------------------------------ 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | #needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.viewcode', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | #templates_path = ['_templates'] 48 | 49 | # The suffix of source filenames. 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | #source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = u'geographiclib' 60 | copyright = u'2021, Charles Karney' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = geographiclib.__version__ 68 | # The full version, including alpha/beta/rc tags. 69 | release = version 70 | today = geographiclib.__release_date__ 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | #language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | #exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = 'classic' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | #html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Output file base name for HTML help builder. 192 | htmlhelp_basename = 'geographiclibdoc' 193 | 194 | # -- Options for LaTeX output --------------------------------------------- 195 | 196 | latex_elements = { 197 | # The paper size ('letterpaper' or 'a4paper'). 198 | #'papersize': 'letterpaper', 199 | 200 | # The font size ('10pt', '11pt' or '12pt'). 201 | #'pointsize': '10pt', 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #'preamble': '', 205 | } 206 | 207 | # Grouping the document tree into LaTeX files. List of tuples 208 | # (source start file, target name, title, 209 | # author, documentclass [howto, manual, or own class]). 210 | latex_documents = [ 211 | ('index', 'geographiclib.tex', u'geographiclib Documentation', 212 | u'Charles Karney', 'manual'), 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | #latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | #latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | #latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | #latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | #latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | #latex_domain_indices = True 234 | 235 | # -- Options for manual page output --------------------------------------- 236 | 237 | # One entry per manual page. List of tuples 238 | # (source start file, name, description, authors, manual section). 239 | man_pages = [ 240 | ('index', 'geographiclib', u'geographiclib Documentation', 241 | [u'Charles Karney'], 1) 242 | ] 243 | 244 | # If true, show URL addresses after external links. 245 | #man_show_urls = False 246 | 247 | # -- Options for Texinfo output ------------------------------------------- 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ('index', 'geographiclib', u'geographiclib Documentation', 254 | u'Charles Karney', 'geographiclib', 'One line description of project.', 255 | 'Miscellaneous'), 256 | ] 257 | 258 | # Documents to append as an appendix to all manuals. 259 | #texinfo_appendices = [] 260 | 261 | # If false, no module index is generated. 262 | #texinfo_domain_indices = True 263 | 264 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 265 | #texinfo_show_urls = 'footnote' 266 | 267 | # If true, do not generate a @detailmenu in the "Top" node's menu. 268 | #texinfo_no_detailmenu = False 269 | 270 | autodoc_member_order = 'bysource' 271 | 272 | autoclass_content = 'both' 273 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Initializing 5 | ------------ 6 | 7 | The following examples all assume that the following commands have been 8 | carried out: 9 | 10 | >>> from geographiclib.geodesic import Geodesic 11 | >>> import math 12 | >>> geod = Geodesic.WGS84 # define the WGS84 ellipsoid 13 | 14 | You can determine the ellipsoid parameters with the *a* and *f* member 15 | variables, for example, 16 | 17 | >>> geod.a, 1/geod.f 18 | (6378137.0, 298.257223563) 19 | 20 | If you need to use a different ellipsoid, construct one by, for example 21 | 22 | >>> geod = Geodesic(6378388, 1/297.0) # the international ellipsoid 23 | 24 | Basic geodesic calculations 25 | --------------------------- 26 | 27 | The distance from Wellington, NZ (41.32S, 174.81E) to Salamanca, Spain 28 | (40.96N, 5.50W) using :meth:`~geographiclib.geodesic.Geodesic.Inverse`: 29 | 30 | >>> g = geod.Inverse(-41.32, 174.81, 40.96, -5.50) 31 | >>> print("The distance is {:.3f} m.".format(g['s12'])) 32 | The distance is 19959679.267 m. 33 | 34 | The point 20000 km SW of Perth, Australia (32.06S, 115.74E) 35 | using :meth:`~geographiclib.geodesic.Geodesic.Direct`: 36 | 37 | >>> g = geod.Direct(-32.06, 115.74, 225, 20000e3) 38 | >>> print("The position is ({:.8f}, {:.8f}).".format(g['lat2'],g['lon2'])) 39 | The position is (32.11195529, -63.95925278). 40 | 41 | The area between the geodesic from JFK Airport (40.6N, 73.8W) to LHR 42 | Airport (51.6N, 0.5W) and the equator. This is an example of setting the 43 | the :ref:`output mask ` parameter. 44 | 45 | >>> g = geod.Inverse(40.6, -73.8, 51.6, -0.5, Geodesic.AREA) 46 | >>> print("The area is {:.1f} m^2".format(g['S12'])) 47 | The area is 40041368848742.5 m^2 48 | 49 | Computing waypoints 50 | ------------------- 51 | 52 | Consider the geodesic between Beijing Airport (40.1N, 116.6E) and San 53 | Fransisco Airport (37.6N, 122.4W). Compute waypoints and azimuths at 54 | intervals of 1000 km using 55 | :meth:`Geodesic.Line ` and 56 | :meth:`GeodesicLine.Position 57 | `: 58 | 59 | >>> l = geod.InverseLine(40.1, 116.6, 37.6, -122.4) 60 | >>> ds = 1000e3; n = int(math.ceil(l.s13 / ds)) 61 | >>> for i in range(n + 1): 62 | ... if i == 0: 63 | ... print("distance latitude longitude azimuth") 64 | ... s = min(ds * i, l.s13) 65 | ... g = l.Position(s, Geodesic.STANDARD | Geodesic.LONG_UNROLL) 66 | ... print("{:.0f} {:.5f} {:.5f} {:.5f}".format( 67 | ... g['s12'], g['lat2'], g['lon2'], g['azi2'])) 68 | ... 69 | distance latitude longitude azimuth 70 | 0 40.10000 116.60000 42.91642 71 | 1000000 46.37321 125.44903 48.99365 72 | 2000000 51.78786 136.40751 57.29433 73 | 3000000 55.92437 149.93825 68.24573 74 | 4000000 58.27452 165.90776 81.68242 75 | 5000000 58.43499 183.03167 96.29014 76 | 6000000 56.37430 199.26948 109.99924 77 | 7000000 52.45769 213.17327 121.33210 78 | 8000000 47.19436 224.47209 129.98619 79 | 9000000 41.02145 233.58294 136.34359 80 | 9513998 37.60000 237.60000 138.89027 81 | 82 | The inclusion of Geodesic.LONG_UNROLL in the call to 83 | GeodesicLine.Position ensures that the longitude does not jump on 84 | crossing the international dateline. 85 | 86 | If the purpose of computing the waypoints is to plot a smooth geodesic, 87 | then it's not important that they be exactly equally spaced. In this 88 | case, it's faster to parameterize the line in terms of the spherical arc 89 | length with :meth:`GeodesicLine.ArcPosition 90 | `. Here the 91 | spacing is about 1° of arc which means that the distance between the 92 | waypoints will be about 60 NM. 93 | 94 | >>> l = geod.InverseLine(40.1, 116.6, 37.6, -122.4, 95 | ... Geodesic.LATITUDE | Geodesic.LONGITUDE) 96 | >>> da = 1; n = int(math.ceil(l.a13 / da)); da = l.a13 / n 97 | >>> for i in range(n + 1): 98 | ... if i == 0: 99 | ... print("latitude longitude") 100 | ... a = da * i 101 | ... g = l.ArcPosition(a, Geodesic.LATITUDE | 102 | ... Geodesic.LONGITUDE | Geodesic.LONG_UNROLL) 103 | ... print("{:.5f} {:.5f}".format(g['lat2'], g['lon2'])) 104 | ... 105 | latitude longitude 106 | 40.10000 116.60000 107 | 40.82573 117.49243 108 | 41.54435 118.40447 109 | 42.25551 119.33686 110 | 42.95886 120.29036 111 | 43.65403 121.26575 112 | 44.34062 122.26380 113 | ... 114 | 39.82385 235.05331 115 | 39.08884 235.91990 116 | 38.34746 236.76857 117 | 37.60000 237.60000 118 | 119 | The variation in the distance between these waypoints is on the order of 120 | 1/*f*. 121 | 122 | Measuring areas 123 | --------------- 124 | 125 | Measure the area of Antarctica using 126 | :meth:`Geodesic.Polygon ` and the 127 | :class:`~geographiclib.polygonarea.PolygonArea` class: 128 | 129 | >>> p = geod.Polygon() 130 | >>> antarctica = [ 131 | ... [-63.1, -58], [-72.9, -74], [-71.9,-102], [-74.9,-102], [-74.3,-131], 132 | ... [-77.5,-163], [-77.4, 163], [-71.7, 172], [-65.9, 140], [-65.7, 113], 133 | ... [-66.6, 88], [-66.9, 59], [-69.8, 25], [-70.0, -4], [-71.0, -14], 134 | ... [-77.3, -33], [-77.9, -46], [-74.7, -61] 135 | ... ] 136 | >>> for pnt in antarctica: 137 | ... p.AddPoint(pnt[0], pnt[1]) 138 | ... 139 | >>> num, perim, area = p.Compute() 140 | >>> print("Perimeter/area of Antarctica are {:.3f} m / {:.1f} m^2". 141 | ... format(perim, area)) 142 | Perimeter/area of Antarctica are 16831067.893 m / 13662703680020.1 m^2 143 | -------------------------------------------------------------------------------- /doc/geodesics.rst: -------------------------------------------------------------------------------- 1 | Geodesics on an ellipsoid 2 | ========================= 3 | 4 | .. _intro: 5 | 6 | Introduction 7 | ------------ 8 | 9 | Consider an ellipsoid of revolution with equatorial radius *a*, polar 10 | semi-axis *b*, and flattening *f* = (*a* − *b*)/*a* . Points on 11 | the surface of the ellipsoid are characterized by their latitude φ 12 | and longitude λ. (Note that latitude here means the 13 | *geographical latitude*, the angle between the normal to the ellipsoid 14 | and the equatorial plane). 15 | 16 | The shortest path between two points on the ellipsoid at 17 | (φ\ :sub:`1`, λ\ :sub:`1`) and (φ\ :sub:`2`, 18 | λ\ :sub:`2`) is called the geodesic. Its length is 19 | *s*\ :sub:`12` and the geodesic from point 1 to point 2 has forward 20 | azimuths α\ :sub:`1` and α\ :sub:`2` at the two end 21 | points. In this figure, we have λ\ :sub:`12` = 22 | λ\ :sub:`2` − λ\ :sub:`1`. 23 | 24 | .. raw:: html 25 | 26 |
27 | Figure from wikipedia 30 |
31 | 32 | A geodesic can be extended indefinitely by requiring that any 33 | sufficiently small segment is a shortest path; geodesics are also the 34 | straightest curves on the surface. 35 | 36 | .. _solution: 37 | 38 | Solution of geodesic problems 39 | ----------------------------- 40 | 41 | Traditionally two geodesic problems are considered: 42 | 43 | * the direct problem — given φ\ :sub:`1`, 44 | λ\ :sub:`1`, α\ :sub:`1`, *s*\ :sub:`12`, 45 | determine φ\ :sub:`2`, λ\ :sub:`2`, and 46 | α\ :sub:`2`; this is solved by 47 | :meth:`Geodesic.Direct `. 48 | 49 | * the inverse problem — given φ\ :sub:`1`, 50 | λ\ :sub:`1`, φ\ :sub:`2`, λ\ :sub:`2`, 51 | determine *s*\ :sub:`12`, α\ :sub:`1`, and 52 | α\ :sub:`2`; this is solved by 53 | :meth:`Geodesic.Inverse `. 54 | 55 | .. _additional: 56 | 57 | Additional properties 58 | --------------------- 59 | 60 | The routines also calculate several other quantities of interest 61 | 62 | * *S*\ :sub:`12` is the area between the geodesic from point 1 to 63 | point 2 and the equator; i.e., it is the area, measured 64 | counter-clockwise, of the quadrilateral with corners 65 | (φ\ :sub:`1`,λ\ :sub:`1`), (0,λ\ :sub:`1`), 66 | (0,λ\ :sub:`2`), and 67 | (φ\ :sub:`2`,λ\ :sub:`2`). It is given in 68 | meters\ :sup:`2`. 69 | * *m*\ :sub:`12`, the reduced length of the geodesic is defined such 70 | that if the initial azimuth is perturbed by *d*\ α\ :sub:`1` 71 | (radians) then the second point is displaced by *m*\ :sub:`12` 72 | *d*\ α\ :sub:`1` in the direction perpendicular to the 73 | geodesic. *m*\ :sub:`12` is given in meters. On a curved surface 74 | the reduced length obeys a symmetry relation, *m*\ :sub:`12` + 75 | *m*\ :sub:`21` = 0. On a flat surface, we have *m*\ :sub:`12` = 76 | *s*\ :sub:`12`. 77 | * *M*\ :sub:`12` and *M*\ :sub:`21` are geodesic scales. If two 78 | geodesics are parallel at point 1 and separated by a small distance 79 | *dt*, then they are separated by a distance *M*\ :sub:`12` *dt* at 80 | point 2. *M*\ :sub:`21` is defined similarly (with the geodesics 81 | being parallel to one another at point 2). *M*\ :sub:`12` and 82 | *M*\ :sub:`21` are dimensionless quantities. On a flat surface, 83 | we have *M*\ :sub:`12` = *M*\ :sub:`21` = 1. 84 | * σ\ :sub:`12` is the arc length on the auxiliary sphere. 85 | This is a construct for converting the problem to one in spherical 86 | trigonometry. The spherical arc length from one equator crossing to 87 | the next is always 180°. 88 | 89 | If points 1, 2, and 3 lie on a single geodesic, then the following 90 | addition rules hold: 91 | 92 | * *s*\ :sub:`13` = *s*\ :sub:`12` + *s*\ :sub:`23` 93 | * σ\ :sub:`13` = σ\ :sub:`12` + σ\ :sub:`23` 94 | * *S*\ :sub:`13` = *S*\ :sub:`12` + *S*\ :sub:`23` 95 | * *m*\ :sub:`13` = *m*\ :sub:`12`\ *M*\ :sub:`23` + 96 | *m*\ :sub:`23`\ *M*\ :sub:`21` 97 | * *M*\ :sub:`13` = *M*\ :sub:`12`\ *M*\ :sub:`23` − 98 | (1 − *M*\ :sub:`12`\ *M*\ :sub:`21`) 99 | *m*\ :sub:`23`/*m*\ :sub:`12` 100 | * *M*\ :sub:`31` = *M*\ :sub:`32`\ *M*\ :sub:`21` − 101 | (1 − *M*\ :sub:`23`\ *M*\ :sub:`32`) 102 | *m*\ :sub:`12`/*m*\ :sub:`23` 103 | 104 | .. _multiple: 105 | 106 | Multiple shortest geodesics 107 | --------------------------- 108 | 109 | The shortest distance found by solving the inverse problem is 110 | (obviously) uniquely defined. However, in a few special cases there are 111 | multiple azimuths which yield the same shortest distance. Here is a 112 | catalog of those cases: 113 | 114 | * φ\ :sub:`1` = −φ\ :sub:`2` (with neither point at 115 | a pole). If α\ :sub:`1` = α\ :sub:`2`, the geodesic 116 | is unique. Otherwise there are two geodesics and the second one is 117 | obtained by setting [α\ :sub:`1`,α\ :sub:`2`] ← 118 | [α\ :sub:`2`,α\ :sub:`1`], 119 | [*M*\ :sub:`12`,\ *M*\ :sub:`21`] ← 120 | [*M*\ :sub:`21`,\ *M*\ :sub:`12`], *S*\ :sub:`12` ← 121 | −\ *S*\ :sub:`12`. (This occurs when the longitude difference 122 | is near ±180° for oblate ellipsoids.) 123 | * λ\ :sub:`2` = λ\ :sub:`1` ± 180° (with 124 | neither point at a pole). If α\ :sub:`1` = 0° or 125 | ±180°, the geodesic is unique. Otherwise there are two 126 | geodesics and the second one is obtained by setting 127 | [α\ :sub:`1`,α\ :sub:`2`] ← 128 | [−α\ :sub:`1`,−α\ :sub:`2`], 129 | *S*\ :sub:`12` ← −\ *S*\ :sub:`12`. (This occurs when 130 | φ\ :sub:`2` is near −φ\ :sub:`1` for prolate 131 | ellipsoids.) 132 | * Points 1 and 2 at opposite poles. There are infinitely many 133 | geodesics which can be generated by setting 134 | [α\ :sub:`1`,α\ :sub:`2`] ← 135 | [α\ :sub:`1`,α\ :sub:`2`] + 136 | [δ,−δ], for arbitrary δ. (For spheres, this 137 | prescription applies when points 1 and 2 are antipodal.) 138 | * *s*\ :sub:`12` = 0 (coincident points). There are infinitely many 139 | geodesics which can be generated by setting 140 | [α\ :sub:`1`,α\ :sub:`2`] ← 141 | [α\ :sub:`1`,α\ :sub:`2`] + [δ,δ], for 142 | arbitrary δ. 143 | 144 | .. _area: 145 | 146 | Area of a polygon 147 | ----------------- 148 | 149 | The area of a geodesic polygon can be determined by summing *S*\ :sub:`12` 150 | for successive edges of the polygon (*S*\ :sub:`12` is negated so that 151 | clockwise traversal of a polygon gives a positive area). However, if 152 | the polygon encircles a pole, the sum must be adjusted by 153 | ±\ *A*/2, where *A* is the area of the full ellipsoid, with 154 | the sign chosen to place the result in (-*A*/2, *A*/2]. 155 | 156 | .. _background: 157 | 158 | Background 159 | ---------- 160 | 161 | The algorithms implemented by this package are given in Karney (2013) 162 | and are based on Bessel (1825) and Helmert (1880); the algorithm for 163 | areas is based on Danielsen (1989). These improve on the work of 164 | Vincenty (1975) in the following respects: 165 | 166 | * The results are accurate to round-off for terrestrial ellipsoids (the 167 | error in the distance is less than 15 nanometers, compared to 0.1 mm 168 | for Vincenty). 169 | * The solution of the inverse problem is always found. (Vincenty's 170 | method fails to converge for nearly antipodal points.) 171 | * The routines calculate differential and integral properties of a 172 | geodesic. This allows, for example, the area of a geodesic polygon to 173 | be computed. 174 | 175 | .. _references: 176 | 177 | References 178 | ---------- 179 | 180 | * F. W. Bessel, 181 | `The calculation of longitude and latitude from geodesic measurements (1825) 182 | `_, 183 | Astron. Nachr. **331**\ (8), 852–861 (2010), 184 | translated by C. F. F. Karney and R. E. Deakin. 185 | * F. R. Helmert, 186 | `Mathematical and Physical Theories of Higher Geodesy, Vol 1 187 | `_, 188 | (Teubner, Leipzig, 1880), Chaps. 5–7. 189 | * T. Vincenty, 190 | `Direct and inverse solutions of geodesics on the ellipsoid with 191 | application of nested equations 192 | `_, 193 | Survey Review **23**\ (176), 88–93 (1975). 194 | * J. Danielsen, 195 | `The area under the geodesic 196 | `_, 197 | Survey Review **30**\ (232), 61–66 (1989). 198 | * C. F. F. Karney, 199 | `Algorithms for geodesics 200 | `_, 201 | J. Geodesy **87**\ (1) 43–55 (2013); 202 | `addenda <../../geod-addenda.html>`_. 203 | * C. F. F. Karney, 204 | `Geodesics on an ellipsoid of revolution 205 | `_, 206 | Feb. 2011; 207 | `errata 208 | <../../geod-addenda.html#geod-errata>`_. 209 | * `A geodesic bibliography 210 | <../../geodesic-papers/biblio.html>`_. 211 | * The wikipedia page, 212 | `Geodesics on an ellipsoid 213 | `_. 214 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. geographiclib documentation master file, created by 2 | sphinx-quickstart on Sat Oct 24 17:14:00 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | geographiclib 7 | ============= 8 | 9 | Author: Charles F. F. Karney (charles@karney.com) 10 | 11 | Version: |version| 12 | 13 | Date: |today| 14 | 15 | This package is a python implementation of the geodesic routines in 16 | `GeographicLib <../../index.html>`_. It allows the solution geodesic 17 | problems on an ellipsoid model of the earth. 18 | 19 | Licensed under the MIT/X11 License; see `LICENSE.txt <../../LICENSE.txt>`_. 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: Contents: 24 | 25 | package 26 | geodesics 27 | interface 28 | code 29 | examples 30 | 31 | Indices and tables 32 | ------------------ 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /doc/interface.rst: -------------------------------------------------------------------------------- 1 | The library interface 2 | ===================== 3 | 4 | Jump to 5 | 6 | * :ref:`units` 7 | * :ref:`dict` 8 | * :ref:`outmask` 9 | * :ref:`restrictions` 10 | 11 | .. _units: 12 | 13 | The units 14 | --------- 15 | 16 | All angles (latitude, longitude, azimuth, arc length) are measured in 17 | degrees with latitudes increasing northwards, longitudes increasing 18 | eastwards, and azimuths measured clockwise from north. For a point at a 19 | pole, the azimuth is defined by keeping the longitude fixed, writing φ = 20 | ±(90° − ε), and taking the limit ε → 0+ 21 | 22 | .. _dict: 23 | 24 | Geodesic dictionary 25 | ------------------- 26 | 27 | The results returned by 28 | :meth:`Geodesic.Direct `, 29 | :meth:`Geodesic.Inverse `, 30 | :meth:`GeodesicLine.Position `, 31 | etc., return a dictionary with some of the following 12 fields set: 32 | 33 | * *lat1* = φ\ :sub:`1`, latitude of point 1 (degrees) 34 | * *lon1* = λ\ :sub:`1`, longitude of point 1 (degrees) 35 | * *azi1* = α\ :sub:`1`, azimuth of line at point 1 (degrees) 36 | * *lat2* = φ\ :sub:`2`, latitude of point 2 (degrees) 37 | * *lon2* = λ\ :sub:`2`, longitude of point 2 (degrees) 38 | * *azi2* = α\ :sub:`2`, (forward) azimuth of line at point 2 (degrees) 39 | * *s12* = *s*\ :sub:`12`, distance from 1 to 2 (meters) 40 | * *a12* = σ\ :sub:`12`, arc length on auxiliary sphere from 1 to 2 (degrees) 41 | * *m12* = *m*\ :sub:`12`, reduced length of geodesic (meters) 42 | * *M12* = *M*\ :sub:`12`, geodesic scale at 2 relative to 1 (dimensionless) 43 | * *M21* = *M*\ :sub:`21`, geodesic scale at 1 relative to 2 (dimensionless) 44 | * *S12* = *S*\ :sub:`12`, area between geodesic and equator (meters\ :sup:`2`) 45 | 46 | .. _outmask: 47 | 48 | *outmask* and *caps* 49 | -------------------- 50 | 51 | By default, the geodesic routines return the 7 basic quantities: *lat1*, 52 | *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, together with the arc 53 | length *a12*. The optional output mask parameter, *outmask*, can be 54 | used to tailor which quantities to calculate. In addition, when a 55 | :class:`~geographiclib.geodesicline.GeodesicLine` is constructed it can 56 | be provided with the optional capabilities parameter, *caps*, which 57 | specifies what quantities can be returned from the resulting object. 58 | 59 | Both *outmask* and *caps* are obtained by or'ing together the following 60 | values 61 | 62 | * :data:`~geographiclib.geodesic.Geodesic.EMPTY`, no capabilities, no output 63 | * :data:`~geographiclib.geodesic.Geodesic.LATITUDE`, compute latitude, *lat2* 64 | * :data:`~geographiclib.geodesic.Geodesic.LONGITUDE`, 65 | compute longitude, *lon2* 66 | * :data:`~geographiclib.geodesic.Geodesic.AZIMUTH`, 67 | compute azimuths, *azi1* and *azi2* 68 | * :data:`~geographiclib.geodesic.Geodesic.DISTANCE`, compute distance, *s12* 69 | * :data:`~geographiclib.geodesic.Geodesic.STANDARD`, all of the above 70 | * :data:`~geographiclib.geodesic.Geodesic.DISTANCE_IN`, 71 | allow *s12* to be used as input in the direct problem 72 | * :data:`~geographiclib.geodesic.Geodesic.REDUCEDLENGTH`, 73 | compute reduced length, *m12* 74 | * :data:`~geographiclib.geodesic.Geodesic.GEODESICSCALE`, 75 | compute geodesic scales, *M12* and *M21* 76 | * :data:`~geographiclib.geodesic.Geodesic.AREA`, compute area, *S12* 77 | * :data:`~geographiclib.geodesic.Geodesic.ALL`, all of the above; 78 | * :data:`~geographiclib.geodesic.Geodesic.LONG_UNROLL`, unroll longitudes 79 | 80 | DISTANCE_IN is a capability provided to the GeodesicLine constructor. It 81 | allows the position on the line to specified in terms of 82 | distance. (Without this, the position can only be specified in terms of 83 | the arc length.) This only makes sense in the *caps* parameter. 84 | 85 | LONG_UNROLL controls the treatment of longitude. If it is not set then 86 | the *lon1* and *lon2* fields are both reduced to the range [−180°, 87 | 180°]. If it is set, then *lon1* is as given in the function call and 88 | (*lon2* − *lon1*) determines how many times and in what sense the 89 | geodesic has encircled the ellipsoid. This only makes sense in the 90 | *outmask* parameter. 91 | 92 | Note that *a12* is always included in the result. 93 | 94 | .. _restrictions: 95 | 96 | Restrictions on the parameters 97 | ------------------------------ 98 | 99 | * Latitudes must lie in [−90°, 90°]. Latitudes outside this range are 100 | replaced by NaNs. 101 | * The distance *s12* is unrestricted. This allows geodesics to wrap 102 | around the ellipsoid. Such geodesics are no longer shortest 103 | paths. However they retain the property that they are the straightest 104 | curves on the surface. 105 | * Similarly, the spherical arc length *a12* is unrestricted. 106 | * Longitudes and azimuths are unrestricted; internally these are 107 | exactly reduced to the range [−180°, 180°]; but see also the 108 | LONG_UNROLL bit. 109 | * The equatorial radius *a* and the polar semi-axis *b* must both be 110 | positive and finite (this implies that −∞ < *f* < 1). 111 | * The flattening *f* should satisfy *f* ∈ [−1/50,1/50] in order to retain 112 | full accuracy. This condition holds for most applications in geodesy. 113 | 114 | Reasonably accurate results can be obtained for −0.2 ≤ *f* ≤ 0.2. Here 115 | is a table of the approximate maximum error (expressed as a distance) 116 | for an ellipsoid with the same equatorial radius as the WGS84 ellipsoid 117 | and different values of the flattening. 118 | 119 | ======== ======= 120 | abs(*f*) error 121 | -------- ------- 122 | 0.003 15 nm 123 | 0.01 25 nm 124 | 0.02 30 nm 125 | 0.05 10 μm 126 | 0.1 1.5 mm 127 | 0.2 300 mm 128 | ======== ======= 129 | 130 | Here 1 nm = 1 nanometer = 10\ :sup:`−9` m (*not* 1 nautical mile!) 131 | -------------------------------------------------------------------------------- /doc/package.rst: -------------------------------------------------------------------------------- 1 | Python package 2 | ============== 3 | 4 | Installation 5 | ------------ 6 | 7 | The full `GeographicLib <../../index.html>`_ package 8 | can be downloaded from 9 | `sourceforge 10 | `_. 11 | However the python implementation is available as a stand-alone package. 12 | To install this, run 13 | 14 | .. code-block:: sh 15 | 16 | pip install geographiclib 17 | 18 | Alternatively downloaded the source package directly from 19 | `Python Package Index `_ 20 | and install it with 21 | 22 | .. code-block:: sh 23 | 24 | pip install geographiclib-2.0.tar.gz 25 | 26 | It's a good idea to run the unit tests to verify that the installation 27 | worked OK by running 28 | 29 | .. code-block:: sh 30 | 31 | python -m unittest geographiclib.test.test_geodesic 32 | 33 | Other links 34 | ----------- 35 | 36 | * Library documentation (all versions): 37 | `https://geographiclib.sourceforge.io/Python <..>`_ 38 | * GIT repository: https://github.com/geographiclib/geographiclib-python 39 | Releases are tagged in git as, e.g., v1.52, v2.0, etc. 40 | * Source distribution: 41 | https://sourceforge.net/projects/geographiclib/files/distrib-Python 42 | * GeographicLib: 43 | `https://geographiclib.sourceforge.io <../../index.html>`_ 44 | * The library has been implemented in a few other 45 | `languages <../../doc/library.html#languages>`_ 46 | 47 | Change log 48 | ---------- 49 | 50 | * Version 2.0 (released 2022-04-23) 51 | 52 | * Minimum version of Python supported in 3.7. Support for Python 2.x 53 | has been dropped. 54 | * This is a major reorganization with the Python package put into its own 55 | git repository, https://github.com/geographiclib/geographiclib-python. 56 | Despite this, there are only reasonably minor changes to the package 57 | itself. 58 | * Fix bug where the solution of the inverse geodesic problem with φ\ 59 | :sub:`1` = 0 and φ\ :sub:`2` = nan was treated as equatorial. 60 | * More careful treatment of ±0° and ±180°. 61 | 62 | * These behave consistently with taking the limits 63 | 64 | * ±0 means ±ε as ε → 0+ 65 | * ±180 means ±(180 − ε) as ε → 0+ 66 | * As a consequence, azimuths of +0° and +180° are reckoned to be 67 | east-going, as far as tracking the longitude with 68 | Geodesic.LONG_UNROLL and the area goes, while azimuths −0° and 69 | −180° are reckoned to be west-going. 70 | * When computing longitude differences, if λ\ :sub:`2` − λ\ :sub:`1` 71 | = ±180° (mod 360°), then the sign is picked depending on the sign 72 | of the difference. 73 | * The normal range for returned longitudes and azimuths is 74 | [−180°, 180°]. 75 | * A separate test suite test_sign has been added to check this 76 | treatment. 77 | 78 | * Version 1.52 (released 2021-06-22) 79 | 80 | * Work around inaccurate math.hypot for python 3.[89] 81 | * Be more aggressive in preventing negative s12 and m12 for short 82 | lines. 83 | 84 | * Version 1.50 (released 2019-09-24) 85 | 86 | * PolygonArea can now handle arbitrarily complex polygons. In the 87 | case of self-intersecting polygons the area is accumulated 88 | "algebraically", e.g., the areas of the 2 loops in a figure-8 89 | polygon will partially cancel. 90 | * Fixed bug in counting pole encirclings when adding edges to a 91 | polygon. 92 | * Work around problems caused by sin(inf) and fmod(inf) raising 93 | exceptions. 94 | * Math.cbrt, Math.atanh, and Math.asinh now preserve the sign of −0. 95 | 96 | * Version 1.49 (released 2017-10-05) 97 | 98 | * Fix code formatting; add tests. 99 | 100 | * Version 1.48 (released 2017-04-09) 101 | 102 | * Change default range for longitude and azimuth to (−180°, 180°] 103 | (instead of [−180°, 180°)). 104 | 105 | * Version 1.47 (released 2017-02-15) 106 | 107 | * Fix the packaging, incorporating the patches in version 1.46.3. 108 | * Improve accuracy of area calculation (fixing a flaw introduced in 109 | version 1.46) 110 | 111 | * Version 1.46 (released 2016-02-15) 112 | 113 | * Add Geodesic.DirectLine, Geodesic.ArcDirectLine, 114 | Geodesic.InverseLine, GeodesicLine.SetDistance, GeodesicLine.SetArc, 115 | GeodesicLine.s13, GeodesicLine.a13. 116 | * More accurate inverse solution when longitude difference is close to 117 | 180°. 118 | * Remove unnecessary functions, CheckPosition, CheckAzimuth, 119 | CheckDistance. 120 | -------------------------------------------------------------------------------- /geographiclib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | configure_file (__init__.py.in __init__.py @ONLY) 2 | foreach (f ${PYTHON_FILES} ${TEST_FILES}) 3 | configure_file (${f} ${f} COPYONLY) 4 | endforeach () 5 | 6 | # linting... 7 | 8 | find_program (LINT pylint) 9 | if (LINT) 10 | set (INDENT " ") 11 | add_custom_target (lint ${LINT} 12 | --max-attributes=36 13 | --max-module-lines=1295 14 | --max-branches=34 15 | --max-args=15 16 | --max-locals=76 17 | --max-statements=175 18 | --max-public-methods=32 19 | --min-public-methods=0 20 | --min-similarity-lines=9 21 | --argument-naming-style=any 22 | --attr-naming-style=any 23 | --method-naming-style=any 24 | --variable-naming-style=any 25 | --indent-string=${INDENT} 26 | # C0321 multiple statements on one line (needed for brevity) 27 | # C0325 unnecessary parens after 'not' keyword (needed for clarity) 28 | # C0415 import outside toplevel (needed because of 29 | # Geodesic+ GeodesicLine interdependency) 30 | # R0124 comparison with self (needed for nan test) 31 | # W0212 access to a protected member 32 | -d C0321,C0325,C0415,R0124,W0212 33 | __init__.py ${PYTHON_FILES} ${TEST_FILES} 34 | COMMENT "Linting with ${LINT}") 35 | endif () 36 | -------------------------------------------------------------------------------- /geographiclib/__init__.py.in: -------------------------------------------------------------------------------- 1 | """geographiclib: geodesic routines from GeographicLib""" 2 | 3 | __version_info__ = (@PROJECT_VERSION_MAJOR@, 4 | @PROJECT_VERSION_MINOR@, 5 | @PROJECT_VERSION_PATCH@) 6 | """GeographicLib version as a tuple""" 7 | 8 | __version__ = "@PROJECT_FULLVERSION@" 9 | """GeographicLib version as a string""" 10 | 11 | __release_date__ = "@RELEASE_DATE@" 12 | """GeographicLib release date""" 13 | -------------------------------------------------------------------------------- /geographiclib/accumulator.py: -------------------------------------------------------------------------------- 1 | """accumulator.py: transcription of GeographicLib::Accumulator class.""" 2 | # accumulator.py 3 | # 4 | # This is a rather literal translation of the GeographicLib::Accumulator class 5 | # from to python. See the documentation for the C++ class for more information 6 | # at 7 | # 8 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 9 | # 10 | # Copyright (c) Charles Karney (2011-2022) and 11 | # licensed under the MIT/X11 License. For more information, see 12 | # https://geographiclib.sourceforge.io/ 13 | ###################################################################### 14 | 15 | from geographiclib.geomath import Math 16 | 17 | class Accumulator: 18 | """Like math.fsum, but allows a running sum""" 19 | 20 | def Set(self, y): 21 | """Set value from argument""" 22 | if isinstance(y, Accumulator): 23 | self._s, self._t = y._s, y._t 24 | else: 25 | self._s, self._t = float(y), 0.0 26 | 27 | def __init__(self, y = 0.0): 28 | """Constructor""" 29 | self._s = self._t = 0.0 30 | self.Set(y) 31 | 32 | def Add(self, y): 33 | """Add a value""" 34 | # Here's Shewchuk's solution... 35 | # hold exact sum as [s, t, u] 36 | y, u = Math.sum(y, self._t) # Accumulate starting at 37 | self._s, self._t = Math.sum(y, self._s) # least significant end 38 | # Start is _s, _t decreasing and non-adjacent. Sum is now (s + t + u) 39 | # exactly with s, t, u non-adjacent and in decreasing order (except 40 | # for possible zeros). The following code tries to normalize the 41 | # result. Ideally, we want _s = round(s+t+u) and _u = round(s+t+u - 42 | # _s). The follow does an approximate job (and maintains the 43 | # decreasing non-adjacent property). Here are two "failures" using 44 | # 3-bit floats: 45 | # 46 | # Case 1: _s is not equal to round(s+t+u) -- off by 1 ulp 47 | # [12, -1] - 8 -> [4, 0, -1] -> [4, -1] = 3 should be [3, 0] = 3 48 | # 49 | # Case 2: _s+_t is not as close to s+t+u as it shold be 50 | # [64, 5] + 4 -> [64, 8, 1] -> [64, 8] = 72 (off by 1) 51 | # should be [80, -7] = 73 (exact) 52 | # 53 | # "Fixing" these problems is probably not worth the expense. The 54 | # representation inevitably leads to small errors in the accumulated 55 | # values. The additional errors illustrated here amount to 1 ulp of 56 | # the less significant word during each addition to the Accumulator 57 | # and an additional possible error of 1 ulp in the reported sum. 58 | # 59 | # Incidentally, the "ideal" representation described above is not 60 | # canonical, because _s = round(_s + _t) may not be true. For 61 | # example, with 3-bit floats: 62 | # 63 | # [128, 16] + 1 -> [160, -16] -- 160 = round(145). 64 | # But [160, 0] - 16 -> [128, 16] -- 128 = round(144). 65 | # 66 | if self._s == 0: # This implies t == 0, 67 | self._s = u # so result is u 68 | else: 69 | self._t += u # otherwise just accumulate u to t. 70 | 71 | def Sum(self, y = 0.0): 72 | """Return sum + y""" 73 | if y == 0.0: 74 | return self._s 75 | b = Accumulator(self) 76 | b.Add(y) 77 | return b._s 78 | 79 | def Negate(self): 80 | """Negate sum""" 81 | self._s *= -1 82 | self._t *= -1 83 | 84 | def Remainder(self, y): 85 | """Remainder on division by y""" 86 | self._s = Math.remainder(self._s, y) 87 | self.Add(0.0) 88 | -------------------------------------------------------------------------------- /geographiclib/constants.py: -------------------------------------------------------------------------------- 1 | """Define the WGS84 ellipsoid""" 2 | # constants.py 3 | # 4 | # This is a translation of the GeographicLib::Constants class to python. See 5 | # the documentation for the C++ class for more information at 6 | # 7 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 8 | # 9 | # Copyright (c) Charles Karney (2011-2022) and 10 | # licensed under the MIT/X11 License. For more information, see 11 | # https://geographiclib.sourceforge.io/ 12 | ###################################################################### 13 | 14 | class Constants: 15 | """ 16 | Constants describing the WGS84 ellipsoid 17 | """ 18 | 19 | WGS84_a = 6378137.0 # meters 20 | """the equatorial radius in meters of the WGS84 ellipsoid in meters""" 21 | WGS84_f = 1/298.257223563 22 | """the flattening of the WGS84 ellipsoid, 1/298.257223563""" 23 | -------------------------------------------------------------------------------- /geographiclib/geodesic.py: -------------------------------------------------------------------------------- 1 | """Define the :class:`~geographiclib.geodesic.Geodesic` class 2 | 3 | The ellipsoid parameters are defined by the constructor. The direct and 4 | inverse geodesic problems are solved by 5 | 6 | * :meth:`~geographiclib.geodesic.Geodesic.Inverse` Solve the inverse 7 | geodesic problem 8 | * :meth:`~geographiclib.geodesic.Geodesic.Direct` Solve the direct 9 | geodesic problem 10 | * :meth:`~geographiclib.geodesic.Geodesic.ArcDirect` Solve the direct 11 | geodesic problem in terms of spherical arc length 12 | 13 | :class:`~geographiclib.geodesicline.GeodesicLine` objects can be created 14 | with 15 | 16 | * :meth:`~geographiclib.geodesic.Geodesic.Line` 17 | * :meth:`~geographiclib.geodesic.Geodesic.DirectLine` 18 | * :meth:`~geographiclib.geodesic.Geodesic.ArcDirectLine` 19 | * :meth:`~geographiclib.geodesic.Geodesic.InverseLine` 20 | 21 | :class:`~geographiclib.polygonarea.PolygonArea` objects can be created 22 | with 23 | 24 | * :meth:`~geographiclib.geodesic.Geodesic.Polygon` 25 | 26 | The public attributes for this class are 27 | 28 | * :attr:`~geographiclib.geodesic.Geodesic.a` 29 | :attr:`~geographiclib.geodesic.Geodesic.f` 30 | 31 | *outmask* and *caps* bit masks are 32 | 33 | * :const:`~geographiclib.geodesic.Geodesic.EMPTY` 34 | * :const:`~geographiclib.geodesic.Geodesic.LATITUDE` 35 | * :const:`~geographiclib.geodesic.Geodesic.LONGITUDE` 36 | * :const:`~geographiclib.geodesic.Geodesic.AZIMUTH` 37 | * :const:`~geographiclib.geodesic.Geodesic.DISTANCE` 38 | * :const:`~geographiclib.geodesic.Geodesic.STANDARD` 39 | * :const:`~geographiclib.geodesic.Geodesic.DISTANCE_IN` 40 | * :const:`~geographiclib.geodesic.Geodesic.REDUCEDLENGTH` 41 | * :const:`~geographiclib.geodesic.Geodesic.GEODESICSCALE` 42 | * :const:`~geographiclib.geodesic.Geodesic.AREA` 43 | * :const:`~geographiclib.geodesic.Geodesic.ALL` 44 | * :const:`~geographiclib.geodesic.Geodesic.LONG_UNROLL` 45 | 46 | :Example: 47 | 48 | >>> from geographiclib.geodesic import Geodesic 49 | >>> # The geodesic inverse problem 50 | ... Geodesic.WGS84.Inverse(-41.32, 174.81, 40.96, -5.50) 51 | {'lat1': -41.32, 52 | 'a12': 179.6197069334283, 53 | 's12': 19959679.26735382, 54 | 'lat2': 40.96, 55 | 'azi2': 18.825195123248392, 56 | 'azi1': 161.06766998615882, 57 | 'lon1': 174.81, 58 | 'lon2': -5.5} 59 | 60 | """ 61 | # geodesic.py 62 | # 63 | # This is a rather literal translation of the GeographicLib::Geodesic class to 64 | # python. See the documentation for the C++ class for more information at 65 | # 66 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 67 | # 68 | # The algorithms are derived in 69 | # 70 | # Charles F. F. Karney, 71 | # Algorithms for geodesics, J. Geodesy 87, 43-55 (2013), 72 | # https://doi.org/10.1007/s00190-012-0578-z 73 | # Addenda: https://geographiclib.sourceforge.io/geod-addenda.html 74 | # 75 | # Copyright (c) Charles Karney (2011-2022) and licensed 76 | # under the MIT/X11 License. For more information, see 77 | # https://geographiclib.sourceforge.io/ 78 | ###################################################################### 79 | 80 | import math 81 | import sys 82 | from geographiclib.geomath import Math 83 | from geographiclib.constants import Constants 84 | from geographiclib.geodesiccapability import GeodesicCapability 85 | 86 | class Geodesic: 87 | """Solve geodesic problems""" 88 | 89 | GEOGRAPHICLIB_GEODESIC_ORDER = 6 90 | nA1_ = GEOGRAPHICLIB_GEODESIC_ORDER 91 | nC1_ = GEOGRAPHICLIB_GEODESIC_ORDER 92 | nC1p_ = GEOGRAPHICLIB_GEODESIC_ORDER 93 | nA2_ = GEOGRAPHICLIB_GEODESIC_ORDER 94 | nC2_ = GEOGRAPHICLIB_GEODESIC_ORDER 95 | nA3_ = GEOGRAPHICLIB_GEODESIC_ORDER 96 | nA3x_ = nA3_ 97 | nC3_ = GEOGRAPHICLIB_GEODESIC_ORDER 98 | nC3x_ = (nC3_ * (nC3_ - 1)) // 2 99 | nC4_ = GEOGRAPHICLIB_GEODESIC_ORDER 100 | nC4x_ = (nC4_ * (nC4_ + 1)) // 2 101 | maxit1_ = 20 102 | maxit2_ = maxit1_ + sys.float_info.mant_dig + 10 103 | 104 | tiny_ = math.sqrt(sys.float_info.min) 105 | tol0_ = sys.float_info.epsilon 106 | tol1_ = 200 * tol0_ 107 | tol2_ = math.sqrt(tol0_) 108 | tolb_ = tol0_ * tol2_ 109 | xthresh_ = 1000 * tol2_ 110 | 111 | CAP_NONE = GeodesicCapability.CAP_NONE 112 | CAP_C1 = GeodesicCapability.CAP_C1 113 | CAP_C1p = GeodesicCapability.CAP_C1p 114 | CAP_C2 = GeodesicCapability.CAP_C2 115 | CAP_C3 = GeodesicCapability.CAP_C3 116 | CAP_C4 = GeodesicCapability.CAP_C4 117 | CAP_ALL = GeodesicCapability.CAP_ALL 118 | CAP_MASK = GeodesicCapability.CAP_MASK 119 | OUT_ALL = GeodesicCapability.OUT_ALL 120 | OUT_MASK = GeodesicCapability.OUT_MASK 121 | 122 | @staticmethod 123 | def _SinCosSeries(sinp, sinx, cosx, c): 124 | """Private: Evaluate a trig series using Clenshaw summation.""" 125 | # Evaluate 126 | # y = sinp ? sum(c[i] * sin( 2*i * x), i, 1, n) : 127 | # sum(c[i] * cos((2*i+1) * x), i, 0, n-1) 128 | # using Clenshaw summation. N.B. c[0] is unused for sin series 129 | # Approx operation count = (n + 5) mult and (2 * n + 2) add 130 | k = len(c) # Point to one beyond last element 131 | n = k - sinp 132 | ar = 2 * (cosx - sinx) * (cosx + sinx) # 2 * cos(2 * x) 133 | y1 = 0 # accumulators for sum 134 | if n & 1: 135 | k -= 1; y0 = c[k] 136 | else: 137 | y0 = 0 138 | # Now n is even 139 | n = n // 2 140 | while n: # while n--: 141 | n -= 1 142 | # Unroll loop x 2, so accumulators return to their original role 143 | k -= 1; y1 = ar * y0 - y1 + c[k] 144 | k -= 1; y0 = ar * y1 - y0 + c[k] 145 | return ( 2 * sinx * cosx * y0 if sinp # sin(2 * x) * y0 146 | else cosx * (y0 - y1) ) # cos(x) * (y0 - y1) 147 | 148 | @staticmethod 149 | def _Astroid(x, y): 150 | """Private: solve astroid equation.""" 151 | # Solve k^4+2*k^3-(x^2+y^2-1)*k^2-2*y^2*k-y^2 = 0 for positive root k. 152 | # This solution is adapted from Geocentric::Reverse. 153 | p = Math.sq(x) 154 | q = Math.sq(y) 155 | r = (p + q - 1) / 6 156 | if not(q == 0 and r <= 0): 157 | # Avoid possible division by zero when r = 0 by multiplying equations 158 | # for s and t by r^3 and r, resp. 159 | S = p * q / 4 # S = r^3 * s 160 | r2 = Math.sq(r) 161 | r3 = r * r2 162 | # The discriminant of the quadratic equation for T3. This is zero on 163 | # the evolute curve p^(1/3)+q^(1/3) = 1 164 | disc = S * (S + 2 * r3) 165 | u = r 166 | if disc >= 0: 167 | T3 = S + r3 168 | # Pick the sign on the sqrt to maximize abs(T3). This minimizes loss 169 | # of precision due to cancellation. The result is unchanged because 170 | # of the way the T is used in definition of u. 171 | T3 += -math.sqrt(disc) if T3 < 0 else math.sqrt(disc) # T3 = (r * t)^3 172 | # N.B. cbrt always returns the real root. cbrt(-8) = -2. 173 | T = Math.cbrt(T3) # T = r * t 174 | # T can be zero; but then r2 / T -> 0. 175 | u += T + (r2 / T if T != 0 else 0) 176 | else: 177 | # T is complex, but the way u is defined the result is real. 178 | ang = math.atan2(math.sqrt(-disc), -(S + r3)) 179 | # There are three possible cube roots. We choose the root which 180 | # avoids cancellation. Note that disc < 0 implies that r < 0. 181 | u += 2 * r * math.cos(ang / 3) 182 | v = math.sqrt(Math.sq(u) + q) # guaranteed positive 183 | # Avoid loss of accuracy when u < 0. 184 | uv = q / (v - u) if u < 0 else u + v # u+v, guaranteed positive 185 | w = (uv - q) / (2 * v) # positive? 186 | # Rearrange expression for k to avoid loss of accuracy due to 187 | # subtraction. Division by 0 not possible because uv > 0, w >= 0. 188 | k = uv / (math.sqrt(uv + Math.sq(w)) + w) # guaranteed positive 189 | else: # q == 0 && r <= 0 190 | # y = 0 with |x| <= 1. Handle this case directly. 191 | # for y small, positive root is k = abs(y)/sqrt(1-x^2) 192 | k = 0 193 | return k 194 | 195 | @staticmethod 196 | def _A1m1f(eps): 197 | """Private: return A1-1.""" 198 | coeff = [ 199 | 1, 4, 64, 0, 256, 200 | ] 201 | m = Geodesic.nA1_//2 202 | t = Math.polyval(m, coeff, 0, Math.sq(eps)) / coeff[m + 1] 203 | return (t + eps) / (1 - eps) 204 | 205 | @staticmethod 206 | def _C1f(eps, c): 207 | """Private: return C1.""" 208 | coeff = [ 209 | -1, 6, -16, 32, 210 | -9, 64, -128, 2048, 211 | 9, -16, 768, 212 | 3, -5, 512, 213 | -7, 1280, 214 | -7, 2048, 215 | ] 216 | eps2 = Math.sq(eps) 217 | d = eps 218 | o = 0 219 | for l in range(1, Geodesic.nC1_ + 1): # l is index of C1p[l] 220 | m = (Geodesic.nC1_ - l) // 2 # order of polynomial in eps^2 221 | c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] 222 | o += m + 2 223 | d *= eps 224 | 225 | @staticmethod 226 | def _C1pf(eps, c): 227 | """Private: return C1'""" 228 | coeff = [ 229 | 205, -432, 768, 1536, 230 | 4005, -4736, 3840, 12288, 231 | -225, 116, 384, 232 | -7173, 2695, 7680, 233 | 3467, 7680, 234 | 38081, 61440, 235 | ] 236 | eps2 = Math.sq(eps) 237 | d = eps 238 | o = 0 239 | for l in range(1, Geodesic.nC1p_ + 1): # l is index of C1p[l] 240 | m = (Geodesic.nC1p_ - l) // 2 # order of polynomial in eps^2 241 | c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] 242 | o += m + 2 243 | d *= eps 244 | 245 | @staticmethod 246 | def _A2m1f(eps): 247 | """Private: return A2-1""" 248 | coeff = [ 249 | -11, -28, -192, 0, 256, 250 | ] 251 | m = Geodesic.nA2_//2 252 | t = Math.polyval(m, coeff, 0, Math.sq(eps)) / coeff[m + 1] 253 | return (t - eps) / (1 + eps) 254 | 255 | @staticmethod 256 | def _C2f(eps, c): 257 | """Private: return C2""" 258 | coeff = [ 259 | 1, 2, 16, 32, 260 | 35, 64, 384, 2048, 261 | 15, 80, 768, 262 | 7, 35, 512, 263 | 63, 1280, 264 | 77, 2048, 265 | ] 266 | eps2 = Math.sq(eps) 267 | d = eps 268 | o = 0 269 | for l in range(1, Geodesic.nC2_ + 1): # l is index of C2[l] 270 | m = (Geodesic.nC2_ - l) // 2 # order of polynomial in eps^2 271 | c[l] = d * Math.polyval(m, coeff, o, eps2) / coeff[o + m + 1] 272 | o += m + 2 273 | d *= eps 274 | 275 | def __init__(self, a, f): 276 | """Construct a Geodesic object 277 | 278 | :param a: the equatorial radius of the ellipsoid in meters 279 | :param f: the flattening of the ellipsoid 280 | 281 | An exception is thrown if *a* or the polar semi-axis *b* = *a* (1 - 282 | *f*) is not a finite positive quantity. 283 | 284 | """ 285 | 286 | self.a = float(a) 287 | """The equatorial radius in meters (readonly)""" 288 | self.f = float(f) 289 | """The flattening (readonly)""" 290 | self._f1 = 1 - self.f 291 | self._e2 = self.f * (2 - self.f) 292 | self._ep2 = self._e2 / Math.sq(self._f1) # e2 / (1 - e2) 293 | self._n = self.f / ( 2 - self.f) 294 | self._b = self.a * self._f1 295 | # authalic radius squared 296 | self._c2 = (Math.sq(self.a) + Math.sq(self._b) * 297 | (1 if self._e2 == 0 else 298 | (math.atanh(math.sqrt(self._e2)) if self._e2 > 0 else 299 | math.atan(math.sqrt(-self._e2))) / 300 | math.sqrt(abs(self._e2))))/2 301 | # The sig12 threshold for "really short". Using the auxiliary sphere 302 | # solution with dnm computed at (bet1 + bet2) / 2, the relative error in 303 | # the azimuth consistency check is sig12^2 * abs(f) * min(1, 1-f/2) / 2. 304 | # (Error measured for 1/100 < b/a < 100 and abs(f) >= 1/1000. For a given 305 | # f and sig12, the max error occurs for lines near the pole. If the old 306 | # rule for computing dnm = (dn1 + dn2)/2 is used, then the error increases 307 | # by a factor of 2.) Setting this equal to epsilon gives sig12 = etol2. 308 | # Here 0.1 is a safety factor (error decreased by 100) and max(0.001, 309 | # abs(f)) stops etol2 getting too large in the nearly spherical case. 310 | self._etol2 = 0.1 * Geodesic.tol2_ / math.sqrt( max(0.001, abs(self.f)) * 311 | min(1.0, 1-self.f/2) / 2 ) 312 | if not(math.isfinite(self.a) and self.a > 0): 313 | raise ValueError("Equatorial radius is not positive") 314 | if not(math.isfinite(self._b) and self._b > 0): 315 | raise ValueError("Polar semi-axis is not positive") 316 | self._A3x = list(range(Geodesic.nA3x_)) 317 | self._C3x = list(range(Geodesic.nC3x_)) 318 | self._C4x = list(range(Geodesic.nC4x_)) 319 | self._A3coeff() 320 | self._C3coeff() 321 | self._C4coeff() 322 | 323 | def _A3coeff(self): 324 | """Private: return coefficients for A3""" 325 | coeff = [ 326 | -3, 128, 327 | -2, -3, 64, 328 | -1, -3, -1, 16, 329 | 3, -1, -2, 8, 330 | 1, -1, 2, 331 | 1, 1, 332 | ] 333 | o = 0; k = 0 334 | for j in range(Geodesic.nA3_ - 1, -1, -1): # coeff of eps^j 335 | m = min(Geodesic.nA3_ - j - 1, j) # order of polynomial in n 336 | self._A3x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] 337 | k += 1 338 | o += m + 2 339 | 340 | def _C3coeff(self): 341 | """Private: return coefficients for C3""" 342 | coeff = [ 343 | 3, 128, 344 | 2, 5, 128, 345 | -1, 3, 3, 64, 346 | -1, 0, 1, 8, 347 | -1, 1, 4, 348 | 5, 256, 349 | 1, 3, 128, 350 | -3, -2, 3, 64, 351 | 1, -3, 2, 32, 352 | 7, 512, 353 | -10, 9, 384, 354 | 5, -9, 5, 192, 355 | 7, 512, 356 | -14, 7, 512, 357 | 21, 2560, 358 | ] 359 | o = 0; k = 0 360 | for l in range(1, Geodesic.nC3_): # l is index of C3[l] 361 | for j in range(Geodesic.nC3_ - 1, l - 1, -1): # coeff of eps^j 362 | m = min(Geodesic.nC3_ - j - 1, j) # order of polynomial in n 363 | self._C3x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] 364 | k += 1 365 | o += m + 2 366 | 367 | def _C4coeff(self): 368 | """Private: return coefficients for C4""" 369 | coeff = [ 370 | 97, 15015, 371 | 1088, 156, 45045, 372 | -224, -4784, 1573, 45045, 373 | -10656, 14144, -4576, -858, 45045, 374 | 64, 624, -4576, 6864, -3003, 15015, 375 | 100, 208, 572, 3432, -12012, 30030, 45045, 376 | 1, 9009, 377 | -2944, 468, 135135, 378 | 5792, 1040, -1287, 135135, 379 | 5952, -11648, 9152, -2574, 135135, 380 | -64, -624, 4576, -6864, 3003, 135135, 381 | 8, 10725, 382 | 1856, -936, 225225, 383 | -8448, 4992, -1144, 225225, 384 | -1440, 4160, -4576, 1716, 225225, 385 | -136, 63063, 386 | 1024, -208, 105105, 387 | 3584, -3328, 1144, 315315, 388 | -128, 135135, 389 | -2560, 832, 405405, 390 | 128, 99099, 391 | ] 392 | o = 0; k = 0 393 | for l in range(Geodesic.nC4_): # l is index of C4[l] 394 | for j in range(Geodesic.nC4_ - 1, l - 1, -1): # coeff of eps^j 395 | m = Geodesic.nC4_ - j - 1 # order of polynomial in n 396 | self._C4x[k] = Math.polyval(m, coeff, o, self._n) / coeff[o + m + 1] 397 | k += 1 398 | o += m + 2 399 | 400 | def _A3f(self, eps): 401 | """Private: return A3""" 402 | # Evaluate A3 403 | return Math.polyval(Geodesic.nA3_ - 1, self._A3x, 0, eps) 404 | 405 | def _C3f(self, eps, c): 406 | """Private: return C3""" 407 | # Evaluate C3 408 | # Elements c[1] thru c[nC3_ - 1] are set 409 | mult = 1 410 | o = 0 411 | for l in range(1, Geodesic.nC3_): # l is index of C3[l] 412 | m = Geodesic.nC3_ - l - 1 # order of polynomial in eps 413 | mult *= eps 414 | c[l] = mult * Math.polyval(m, self._C3x, o, eps) 415 | o += m + 1 416 | 417 | def _C4f(self, eps, c): 418 | """Private: return C4""" 419 | # Evaluate C4 coeffs by Horner's method 420 | # Elements c[0] thru c[nC4_ - 1] are set 421 | mult = 1 422 | o = 0 423 | for l in range(Geodesic.nC4_): # l is index of C4[l] 424 | m = Geodesic.nC4_ - l - 1 # order of polynomial in eps 425 | c[l] = mult * Math.polyval(m, self._C4x, o, eps) 426 | o += m + 1 427 | mult *= eps 428 | 429 | # return s12b, m12b, m0, M12, M21 430 | def _Lengths(self, eps, sig12, 431 | ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, outmask, 432 | # Scratch areas of the right size 433 | C1a, C2a): 434 | """Private: return a bunch of lengths""" 435 | # Return s12b, m12b, m0, M12, M21, where 436 | # m12b = (reduced length)/_b; s12b = distance/_b, 437 | # m0 = coefficient of secular term in expression for reduced length. 438 | outmask &= Geodesic.OUT_MASK 439 | # outmask & DISTANCE: set s12b 440 | # outmask & REDUCEDLENGTH: set m12b & m0 441 | # outmask & GEODESICSCALE: set M12 & M21 442 | 443 | s12b = m12b = m0 = M12 = M21 = math.nan 444 | if outmask & (Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | 445 | Geodesic.GEODESICSCALE): 446 | A1 = Geodesic._A1m1f(eps) 447 | Geodesic._C1f(eps, C1a) 448 | if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): 449 | A2 = Geodesic._A2m1f(eps) 450 | Geodesic._C2f(eps, C2a) 451 | m0x = A1 - A2 452 | A2 = 1 + A2 453 | A1 = 1 + A1 454 | if outmask & Geodesic.DISTANCE: 455 | B1 = (Geodesic._SinCosSeries(True, ssig2, csig2, C1a) - 456 | Geodesic._SinCosSeries(True, ssig1, csig1, C1a)) 457 | # Missing a factor of _b 458 | s12b = A1 * (sig12 + B1) 459 | if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): 460 | B2 = (Geodesic._SinCosSeries(True, ssig2, csig2, C2a) - 461 | Geodesic._SinCosSeries(True, ssig1, csig1, C2a)) 462 | J12 = m0x * sig12 + (A1 * B1 - A2 * B2) 463 | elif outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): 464 | # Assume here that nC1_ >= nC2_ 465 | for l in range(1, Geodesic.nC2_): 466 | C2a[l] = A1 * C1a[l] - A2 * C2a[l] 467 | J12 = m0x * sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, C2a) - 468 | Geodesic._SinCosSeries(True, ssig1, csig1, C2a)) 469 | if outmask & Geodesic.REDUCEDLENGTH: 470 | m0 = m0x 471 | # Missing a factor of _b. 472 | # Add parens around (csig1 * ssig2) and (ssig1 * csig2) to ensure 473 | # accurate cancellation in the case of coincident points. 474 | m12b = (dn2 * (csig1 * ssig2) - dn1 * (ssig1 * csig2) - 475 | csig1 * csig2 * J12) 476 | if outmask & Geodesic.GEODESICSCALE: 477 | csig12 = csig1 * csig2 + ssig1 * ssig2 478 | t = self._ep2 * (cbet1 - cbet2) * (cbet1 + cbet2) / (dn1 + dn2) 479 | M12 = csig12 + (t * ssig2 - csig2 * J12) * ssig1 / dn1 480 | M21 = csig12 - (t * ssig1 - csig1 * J12) * ssig2 / dn2 481 | return s12b, m12b, m0, M12, M21 482 | 483 | # return sig12, salp1, calp1, salp2, calp2, dnm 484 | def _InverseStart(self, sbet1, cbet1, dn1, sbet2, cbet2, dn2, 485 | lam12, slam12, clam12, 486 | # Scratch areas of the right size 487 | C1a, C2a): 488 | """Private: Find a starting value for Newton's method.""" 489 | # Return a starting point for Newton's method in salp1 and calp1 (function 490 | # value is -1). If Newton's method doesn't need to be used, return also 491 | # salp2 and calp2 and function value is sig12. 492 | sig12 = -1; salp2 = calp2 = dnm = math.nan # Return values 493 | # bet12 = bet2 - bet1 in [0, pi); bet12a = bet2 + bet1 in (-pi, 0] 494 | sbet12 = sbet2 * cbet1 - cbet2 * sbet1 495 | cbet12 = cbet2 * cbet1 + sbet2 * sbet1 496 | # Volatile declaration needed to fix inverse cases 497 | # 88.202499451857 0 -88.202499451857 179.981022032992859592 498 | # 89.262080389218 0 -89.262080389218 179.992207982775375662 499 | # 89.333123580033 0 -89.333123580032997687 179.99295812360148422 500 | # which otherwise fail with g++ 4.4.4 x86 -O3 501 | sbet12a = sbet2 * cbet1 502 | sbet12a += cbet2 * sbet1 503 | 504 | shortline = cbet12 >= 0 and sbet12 < 0.5 and cbet2 * lam12 < 0.5 505 | if shortline: 506 | sbetm2 = Math.sq(sbet1 + sbet2) 507 | # sin((bet1+bet2)/2)^2 508 | # = (sbet1 + sbet2)^2 / ((sbet1 + sbet2)^2 + (cbet1 + cbet2)^2) 509 | sbetm2 /= sbetm2 + Math.sq(cbet1 + cbet2) 510 | dnm = math.sqrt(1 + self._ep2 * sbetm2) 511 | omg12 = lam12 / (self._f1 * dnm) 512 | somg12 = math.sin(omg12); comg12 = math.cos(omg12) 513 | else: 514 | somg12 = slam12; comg12 = clam12 515 | 516 | salp1 = cbet2 * somg12 517 | calp1 = ( 518 | sbet12 + cbet2 * sbet1 * Math.sq(somg12) / (1 + comg12) if comg12 >= 0 519 | else sbet12a - cbet2 * sbet1 * Math.sq(somg12) / (1 - comg12)) 520 | 521 | ssig12 = math.hypot(salp1, calp1) 522 | csig12 = sbet1 * sbet2 + cbet1 * cbet2 * comg12 523 | 524 | if shortline and ssig12 < self._etol2: 525 | # really short lines 526 | salp2 = cbet1 * somg12 527 | calp2 = sbet12 - cbet1 * sbet2 * (Math.sq(somg12) / (1 + comg12) 528 | if comg12 >= 0 else 1 - comg12) 529 | salp2, calp2 = Math.norm(salp2, calp2) 530 | # Set return value 531 | sig12 = math.atan2(ssig12, csig12) 532 | elif (abs(self._n) >= 0.1 or # Skip astroid calc if too eccentric 533 | csig12 >= 0 or 534 | ssig12 >= 6 * abs(self._n) * math.pi * Math.sq(cbet1)): 535 | # Nothing to do, zeroth order spherical approximation is OK 536 | pass 537 | else: 538 | # Scale lam12 and bet2 to x, y coordinate system where antipodal point 539 | # is at origin and singular point is at y = 0, x = -1. 540 | # real y, lamscale, betscale 541 | lam12x = math.atan2(-slam12, -clam12) 542 | if self.f >= 0: # In fact f == 0 does not get here 543 | # x = dlong, y = dlat 544 | k2 = Math.sq(sbet1) * self._ep2 545 | eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) 546 | lamscale = self.f * cbet1 * self._A3f(eps) * math.pi 547 | betscale = lamscale * cbet1 548 | x = lam12x / lamscale 549 | y = sbet12a / betscale 550 | else: # _f < 0 551 | # x = dlat, y = dlong 552 | cbet12a = cbet2 * cbet1 - sbet2 * sbet1 553 | bet12a = math.atan2(sbet12a, cbet12a) 554 | # real m12b, m0, dummy 555 | # In the case of lon12 = 180, this repeats a calculation made in 556 | # Inverse. 557 | dummy, m12b, m0, dummy, dummy = self._Lengths( 558 | self._n, math.pi + bet12a, sbet1, -cbet1, dn1, sbet2, cbet2, dn2, 559 | cbet1, cbet2, Geodesic.REDUCEDLENGTH, C1a, C2a) 560 | x = -1 + m12b / (cbet1 * cbet2 * m0 * math.pi) 561 | betscale = (sbet12a / x if x < -0.01 562 | else -self.f * Math.sq(cbet1) * math.pi) 563 | lamscale = betscale / cbet1 564 | y = lam12x / lamscale 565 | 566 | if y > -Geodesic.tol1_ and x > -1 - Geodesic.xthresh_: 567 | # strip near cut 568 | if self.f >= 0: 569 | salp1 = min(1.0, -x); calp1 = - math.sqrt(1 - Math.sq(salp1)) 570 | else: 571 | calp1 = max((0.0 if x > -Geodesic.tol1_ else -1.0), x) 572 | salp1 = math.sqrt(1 - Math.sq(calp1)) 573 | else: 574 | # Estimate alp1, by solving the astroid problem. 575 | # 576 | # Could estimate alpha1 = theta + pi/2, directly, i.e., 577 | # calp1 = y/k; salp1 = -x/(1+k); for _f >= 0 578 | # calp1 = x/(1+k); salp1 = -y/k; for _f < 0 (need to check) 579 | # 580 | # However, it's better to estimate omg12 from astroid and use 581 | # spherical formula to compute alp1. This reduces the mean number of 582 | # Newton iterations for astroid cases from 2.24 (min 0, max 6) to 2.12 583 | # (min 0 max 5). The changes in the number of iterations are as 584 | # follows: 585 | # 586 | # change percent 587 | # 1 5 588 | # 0 78 589 | # -1 16 590 | # -2 0.6 591 | # -3 0.04 592 | # -4 0.002 593 | # 594 | # The histogram of iterations is (m = number of iterations estimating 595 | # alp1 directly, n = number of iterations estimating via omg12, total 596 | # number of trials = 148605): 597 | # 598 | # iter m n 599 | # 0 148 186 600 | # 1 13046 13845 601 | # 2 93315 102225 602 | # 3 36189 32341 603 | # 4 5396 7 604 | # 5 455 1 605 | # 6 56 0 606 | # 607 | # Because omg12 is near pi, estimate work with omg12a = pi - omg12 608 | k = Geodesic._Astroid(x, y) 609 | omg12a = lamscale * ( -x * k/(1 + k) if self.f >= 0 610 | else -y * (1 + k)/k ) 611 | somg12 = math.sin(omg12a); comg12 = -math.cos(omg12a) 612 | # Update spherical estimate of alp1 using omg12 instead of lam12 613 | salp1 = cbet2 * somg12 614 | calp1 = sbet12a - cbet2 * sbet1 * Math.sq(somg12) / (1 - comg12) 615 | # Sanity check on starting guess. Backwards check allows NaN through. 616 | if not (salp1 <= 0): 617 | salp1, calp1 = Math.norm(salp1, calp1) 618 | else: 619 | salp1 = 1; calp1 = 0 620 | return sig12, salp1, calp1, salp2, calp2, dnm 621 | 622 | # return lam12, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, eps, 623 | # domg12, dlam12 624 | def _Lambda12(self, sbet1, cbet1, dn1, sbet2, cbet2, dn2, salp1, calp1, 625 | slam120, clam120, diffp, 626 | # Scratch areas of the right size 627 | C1a, C2a, C3a): 628 | """Private: Solve hybrid problem""" 629 | if sbet1 == 0 and calp1 == 0: 630 | # Break degeneracy of equatorial line. This case has already been 631 | # handled. 632 | calp1 = -Geodesic.tiny_ 633 | 634 | # sin(alp1) * cos(bet1) = sin(alp0) 635 | salp0 = salp1 * cbet1 636 | calp0 = math.hypot(calp1, salp1 * sbet1) # calp0 > 0 637 | 638 | # real somg1, comg1, somg2, comg2, lam12 639 | # tan(bet1) = tan(sig1) * cos(alp1) 640 | # tan(omg1) = sin(alp0) * tan(sig1) = tan(omg1)=tan(alp1)*sin(bet1) 641 | ssig1 = sbet1; somg1 = salp0 * sbet1 642 | csig1 = comg1 = calp1 * cbet1 643 | ssig1, csig1 = Math.norm(ssig1, csig1) 644 | # Math.norm(somg1, comg1); -- don't need to normalize! 645 | 646 | # Enforce symmetries in the case abs(bet2) = -bet1. Need to be careful 647 | # about this case, since this can yield singularities in the Newton 648 | # iteration. 649 | # sin(alp2) * cos(bet2) = sin(alp0) 650 | salp2 = salp0 / cbet2 if cbet2 != cbet1 else salp1 651 | # calp2 = sqrt(1 - sq(salp2)) 652 | # = sqrt(sq(calp0) - sq(sbet2)) / cbet2 653 | # and subst for calp0 and rearrange to give (choose positive sqrt 654 | # to give alp2 in [0, pi/2]). 655 | calp2 = (math.sqrt(Math.sq(calp1 * cbet1) + 656 | ((cbet2 - cbet1) * (cbet1 + cbet2) if cbet1 < -sbet1 657 | else (sbet1 - sbet2) * (sbet1 + sbet2))) / cbet2 658 | if cbet2 != cbet1 or abs(sbet2) != -sbet1 else abs(calp1)) 659 | # tan(bet2) = tan(sig2) * cos(alp2) 660 | # tan(omg2) = sin(alp0) * tan(sig2). 661 | ssig2 = sbet2; somg2 = salp0 * sbet2 662 | csig2 = comg2 = calp2 * cbet2 663 | ssig2, csig2 = Math.norm(ssig2, csig2) 664 | # Math.norm(somg2, comg2); -- don't need to normalize! 665 | 666 | # sig12 = sig2 - sig1, limit to [0, pi] 667 | sig12 = math.atan2(max(0.0, csig1 * ssig2 - ssig1 * csig2) + 0.0, 668 | csig1 * csig2 + ssig1 * ssig2) 669 | 670 | # omg12 = omg2 - omg1, limit to [0, pi] 671 | somg12 = max(0.0, comg1 * somg2 - somg1 * comg2) + 0.0 672 | comg12 = comg1 * comg2 + somg1 * somg2 673 | # eta = omg12 - lam120 674 | eta = math.atan2(somg12 * clam120 - comg12 * slam120, 675 | comg12 * clam120 + somg12 * slam120) 676 | 677 | # real B312 678 | k2 = Math.sq(calp0) * self._ep2 679 | eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) 680 | self._C3f(eps, C3a) 681 | B312 = (Geodesic._SinCosSeries(True, ssig2, csig2, C3a) - 682 | Geodesic._SinCosSeries(True, ssig1, csig1, C3a)) 683 | domg12 = -self.f * self._A3f(eps) * salp0 * (sig12 + B312) 684 | lam12 = eta + domg12 685 | 686 | if diffp: 687 | if calp2 == 0: 688 | dlam12 = - 2 * self._f1 * dn1 / sbet1 689 | else: 690 | dummy, dlam12, dummy, dummy, dummy = self._Lengths( 691 | eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, 692 | Geodesic.REDUCEDLENGTH, C1a, C2a) 693 | dlam12 *= self._f1 / (calp2 * cbet2) 694 | else: 695 | dlam12 = math.nan 696 | 697 | return (lam12, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, eps, 698 | domg12, dlam12) 699 | 700 | # return a12, s12, salp1, calp1, salp2, calp2, m12, M12, M21, S12 701 | def _GenInverse(self, lat1, lon1, lat2, lon2, outmask): 702 | """Private: General version of the inverse problem""" 703 | a12 = s12 = m12 = M12 = M21 = S12 = math.nan # return vals 704 | 705 | outmask &= Geodesic.OUT_MASK 706 | # Compute longitude difference (AngDiff does this carefully). Result is 707 | # in [-180, 180] but -180 is only for west-going geodesics. 180 is for 708 | # east-going and meridional geodesics. 709 | lon12, lon12s = Math.AngDiff(lon1, lon2) 710 | # Make longitude difference positive. 711 | lonsign = math.copysign(1, lon12) 712 | lon12 = lonsign * lon12; lon12s = lonsign * lon12s 713 | lam12 = math.radians(lon12) 714 | # Calculate sincos of lon12 + error (this applies AngRound internally). 715 | slam12, clam12 = Math.sincosde(lon12, lon12s) 716 | lon12s = (180 - lon12) - lon12s # the supplementary longitude difference 717 | 718 | # If really close to the equator, treat as on equator. 719 | lat1 = Math.AngRound(Math.LatFix(lat1)) 720 | lat2 = Math.AngRound(Math.LatFix(lat2)) 721 | # Swap points so that point with higher (abs) latitude is point 1 722 | # If one latitude is a nan, then it becomes lat1. 723 | swapp = -1 if abs(lat1) < abs(lat2) or math.isnan(lat2) else 1 724 | if swapp < 0: 725 | lonsign *= -1 726 | lat2, lat1 = lat1, lat2 727 | # Make lat1 <= 0 728 | latsign = math.copysign(1, -lat1) 729 | lat1 *= latsign 730 | lat2 *= latsign 731 | # Now we have 732 | # 733 | # 0 <= lon12 <= 180 734 | # -90 <= lat1 <= 0 735 | # lat1 <= lat2 <= -lat1 736 | # 737 | # longsign, swapp, latsign register the transformation to bring the 738 | # coordinates to this canonical form. In all cases, 1 means no change was 739 | # made. We make these transformations so that there are few cases to 740 | # check, e.g., on verifying quadrants in atan2. In addition, this 741 | # enforces some symmetries in the results returned. 742 | 743 | # real phi, sbet1, cbet1, sbet2, cbet2, s12x, m12x 744 | 745 | sbet1, cbet1 = Math.sincosd(lat1); sbet1 *= self._f1 746 | # Ensure cbet1 = +epsilon at poles 747 | sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1) 748 | 749 | sbet2, cbet2 = Math.sincosd(lat2); sbet2 *= self._f1 750 | # Ensure cbet2 = +epsilon at poles 751 | sbet2, cbet2 = Math.norm(sbet2, cbet2); cbet2 = max(Geodesic.tiny_, cbet2) 752 | 753 | # If cbet1 < -sbet1, then cbet2 - cbet1 is a sensitive measure of the 754 | # |bet1| - |bet2|. Alternatively (cbet1 >= -sbet1), abs(sbet2) + sbet1 is 755 | # a better measure. This logic is used in assigning calp2 in Lambda12. 756 | # Sometimes these quantities vanish and in that case we force bet2 = +/- 757 | # bet1 exactly. An example where is is necessary is the inverse problem 758 | # 48.522876735459 0 -48.52287673545898293 179.599720456223079643 759 | # which failed with Visual Studio 10 (Release and Debug) 760 | 761 | if cbet1 < -sbet1: 762 | if cbet2 == cbet1: 763 | sbet2 = math.copysign(sbet1, sbet2) 764 | else: 765 | if abs(sbet2) == -sbet1: 766 | cbet2 = cbet1 767 | 768 | dn1 = math.sqrt(1 + self._ep2 * Math.sq(sbet1)) 769 | dn2 = math.sqrt(1 + self._ep2 * Math.sq(sbet2)) 770 | 771 | # real a12, sig12, calp1, salp1, calp2, salp2 772 | # index zero elements of these arrays are unused 773 | C1a = list(range(Geodesic.nC1_ + 1)) 774 | C2a = list(range(Geodesic.nC2_ + 1)) 775 | C3a = list(range(Geodesic.nC3_)) 776 | 777 | meridian = lat1 == -90 or slam12 == 0 778 | 779 | if meridian: 780 | 781 | # Endpoints are on a single full meridian, so the geodesic might lie on 782 | # a meridian. 783 | 784 | calp1 = clam12; salp1 = slam12 # Head to the target longitude 785 | calp2 = 1.0; salp2 = 0.0 # At the target we're heading north 786 | 787 | # tan(bet) = tan(sig) * cos(alp) 788 | ssig1 = sbet1; csig1 = calp1 * cbet1 789 | ssig2 = sbet2; csig2 = calp2 * cbet2 790 | 791 | # sig12 = sig2 - sig1 792 | sig12 = math.atan2(max(0.0, csig1 * ssig2 - ssig1 * csig2) + 0.0, 793 | csig1 * csig2 + ssig1 * ssig2) 794 | 795 | s12x, m12x, dummy, M12, M21 = self._Lengths( 796 | self._n, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, 797 | outmask | Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH, C1a, C2a) 798 | 799 | # Add the check for sig12 since zero length geodesics might yield m12 < 800 | # 0. Test case was 801 | # 802 | # echo 20.001 0 20.001 0 | GeodSolve -i 803 | # 804 | # In fact, we will have sig12 > pi/2 for meridional geodesic which is 805 | # not a shortest path. 806 | if sig12 < 1 or m12x >= 0: 807 | if (sig12 < 3 * Geodesic.tiny_ or 808 | # Prevent negative s12 or m12 for short lines 809 | (sig12 < Geodesic.tol0_ and (s12x < 0 or m12x < 0))): 810 | sig12 = m12x = s12x = 0.0 811 | m12x *= self._b 812 | s12x *= self._b 813 | a12 = math.degrees(sig12) 814 | else: 815 | # m12 < 0, i.e., prolate and too close to anti-podal 816 | meridian = False 817 | # end if meridian: 818 | 819 | # somg12 == 2 marks that it needs to be calculated 820 | somg12 = 2.0; comg12 = 0.0; omg12 = 0.0 821 | if (not meridian and 822 | sbet1 == 0 and # and sbet2 == 0 823 | # Mimic the way Lambda12 works with calp1 = 0 824 | (self.f <= 0 or lon12s >= self.f * 180)): 825 | 826 | # Geodesic runs along equator 827 | calp1 = calp2 = 0.0; salp1 = salp2 = 1.0 828 | s12x = self.a * lam12 829 | sig12 = omg12 = lam12 / self._f1 830 | m12x = self._b * math.sin(sig12) 831 | if outmask & Geodesic.GEODESICSCALE: 832 | M12 = M21 = math.cos(sig12) 833 | a12 = lon12 / self._f1 834 | 835 | elif not meridian: 836 | 837 | # Now point1 and point2 belong within a hemisphere bounded by a 838 | # meridian and geodesic is neither meridional or equatorial. 839 | 840 | # Figure a starting point for Newton's method 841 | sig12, salp1, calp1, salp2, calp2, dnm = self._InverseStart( 842 | sbet1, cbet1, dn1, sbet2, cbet2, dn2, lam12, slam12, clam12, C1a, C2a) 843 | 844 | if sig12 >= 0: 845 | # Short lines (InverseStart sets salp2, calp2, dnm) 846 | s12x = sig12 * self._b * dnm 847 | m12x = (Math.sq(dnm) * self._b * math.sin(sig12 / dnm)) 848 | if outmask & Geodesic.GEODESICSCALE: 849 | M12 = M21 = math.cos(sig12 / dnm) 850 | a12 = math.degrees(sig12) 851 | omg12 = lam12 / (self._f1 * dnm) 852 | else: 853 | 854 | # Newton's method. This is a straightforward solution of f(alp1) = 855 | # lambda12(alp1) - lam12 = 0 with one wrinkle. f(alp) has exactly one 856 | # root in the interval (0, pi) and its derivative is positive at the 857 | # root. Thus f(alp) is positive for alp > alp1 and negative for alp < 858 | # alp1. During the course of the iteration, a range (alp1a, alp1b) is 859 | # maintained which brackets the root and with each evaluation of f(alp) 860 | # the range is shrunk if possible. Newton's method is restarted 861 | # whenever the derivative of f is negative (because the new value of 862 | # alp1 is then further from the solution) or if the new estimate of 863 | # alp1 lies outside (0,pi); in this case, the new starting guess is 864 | # taken to be (alp1a + alp1b) / 2. 865 | # real ssig1, csig1, ssig2, csig2, eps 866 | numit = 0 867 | tripn = tripb = False 868 | # Bracketing range 869 | salp1a = Geodesic.tiny_; calp1a = 1.0 870 | salp1b = Geodesic.tiny_; calp1b = -1.0 871 | 872 | while numit < Geodesic.maxit2_: 873 | # the WGS84 test set: mean = 1.47, sd = 1.25, max = 16 874 | # WGS84 and random input: mean = 2.85, sd = 0.60 875 | (v, salp2, calp2, sig12, ssig1, csig1, ssig2, csig2, 876 | eps, domg12, dv) = self._Lambda12( 877 | sbet1, cbet1, dn1, sbet2, cbet2, dn2, 878 | salp1, calp1, slam12, clam12, numit < Geodesic.maxit1_, 879 | C1a, C2a, C3a) 880 | # Reversed test to allow escape with NaNs 881 | if tripb or not (abs(v) >= (8 if tripn else 1) * Geodesic.tol0_): 882 | break 883 | # Update bracketing values 884 | if v > 0 and (numit > Geodesic.maxit1_ or 885 | calp1/salp1 > calp1b/salp1b): 886 | salp1b = salp1; calp1b = calp1 887 | elif v < 0 and (numit > Geodesic.maxit1_ or 888 | calp1/salp1 < calp1a/salp1a): 889 | salp1a = salp1; calp1a = calp1 890 | 891 | numit += 1 892 | if numit < Geodesic.maxit1_ and dv > 0: 893 | dalp1 = -v/dv 894 | sdalp1 = math.sin(dalp1); cdalp1 = math.cos(dalp1) 895 | nsalp1 = salp1 * cdalp1 + calp1 * sdalp1 896 | if nsalp1 > 0 and abs(dalp1) < math.pi: 897 | calp1 = calp1 * cdalp1 - salp1 * sdalp1 898 | salp1 = nsalp1 899 | salp1, calp1 = Math.norm(salp1, calp1) 900 | # In some regimes we don't get quadratic convergence because 901 | # slope -> 0. So use convergence conditions based on epsilon 902 | # instead of sqrt(epsilon). 903 | tripn = abs(v) <= 16 * Geodesic.tol0_ 904 | continue 905 | # Either dv was not positive or updated value was outside 906 | # legal range. Use the midpoint of the bracket as the next 907 | # estimate. This mechanism is not needed for the WGS84 908 | # ellipsoid, but it does catch problems with more eccentric 909 | # ellipsoids. Its efficacy is such for 910 | # the WGS84 test set with the starting guess set to alp1 = 90deg: 911 | # the WGS84 test set: mean = 5.21, sd = 3.93, max = 24 912 | # WGS84 and random input: mean = 4.74, sd = 0.99 913 | salp1 = (salp1a + salp1b)/2 914 | calp1 = (calp1a + calp1b)/2 915 | salp1, calp1 = Math.norm(salp1, calp1) 916 | tripn = False 917 | tripb = (abs(salp1a - salp1) + (calp1a - calp1) < Geodesic.tolb_ or 918 | abs(salp1 - salp1b) + (calp1 - calp1b) < Geodesic.tolb_) 919 | 920 | lengthmask = (outmask | 921 | (Geodesic.DISTANCE 922 | if (outmask & (Geodesic.REDUCEDLENGTH | 923 | Geodesic.GEODESICSCALE)) 924 | else Geodesic.EMPTY)) 925 | s12x, m12x, dummy, M12, M21 = self._Lengths( 926 | eps, sig12, ssig1, csig1, dn1, ssig2, csig2, dn2, cbet1, cbet2, 927 | lengthmask, C1a, C2a) 928 | 929 | m12x *= self._b 930 | s12x *= self._b 931 | a12 = math.degrees(sig12) 932 | if outmask & Geodesic.AREA: 933 | # omg12 = lam12 - domg12 934 | sdomg12 = math.sin(domg12); cdomg12 = math.cos(domg12) 935 | somg12 = slam12 * cdomg12 - clam12 * sdomg12 936 | comg12 = clam12 * cdomg12 + slam12 * sdomg12 937 | 938 | # end elif not meridian 939 | 940 | if outmask & Geodesic.DISTANCE: 941 | s12 = 0.0 + s12x # Convert -0 to 0 942 | 943 | if outmask & Geodesic.REDUCEDLENGTH: 944 | m12 = 0.0 + m12x # Convert -0 to 0 945 | 946 | if outmask & Geodesic.AREA: 947 | # From Lambda12: sin(alp1) * cos(bet1) = sin(alp0) 948 | salp0 = salp1 * cbet1 949 | calp0 = math.hypot(calp1, salp1 * sbet1) # calp0 > 0 950 | # real alp12 951 | if calp0 != 0 and salp0 != 0: 952 | # From Lambda12: tan(bet) = tan(sig) * cos(alp) 953 | ssig1 = sbet1; csig1 = calp1 * cbet1 954 | ssig2 = sbet2; csig2 = calp2 * cbet2 955 | k2 = Math.sq(calp0) * self._ep2 956 | eps = k2 / (2 * (1 + math.sqrt(1 + k2)) + k2) 957 | # Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0). 958 | A4 = Math.sq(self.a) * calp0 * salp0 * self._e2 959 | ssig1, csig1 = Math.norm(ssig1, csig1) 960 | ssig2, csig2 = Math.norm(ssig2, csig2) 961 | C4a = list(range(Geodesic.nC4_)) 962 | self._C4f(eps, C4a) 963 | B41 = Geodesic._SinCosSeries(False, ssig1, csig1, C4a) 964 | B42 = Geodesic._SinCosSeries(False, ssig2, csig2, C4a) 965 | S12 = A4 * (B42 - B41) 966 | else: 967 | # Avoid problems with indeterminate sig1, sig2 on equator 968 | S12 = 0.0 969 | 970 | if not meridian and somg12 == 2.0: 971 | somg12 = math.sin(omg12); comg12 = math.cos(omg12) 972 | 973 | if (not meridian and 974 | # omg12 < 3/4 * pi 975 | comg12 > -0.7071 and # Long difference not too big 976 | sbet2 - sbet1 < 1.75): # Lat difference not too big 977 | # Use tan(Gamma/2) = tan(omg12/2) 978 | # * (tan(bet1/2)+tan(bet2/2))/(1+tan(bet1/2)*tan(bet2/2)) 979 | # with tan(x/2) = sin(x)/(1+cos(x)) 980 | domg12 = 1 + comg12; dbet1 = 1 + cbet1; dbet2 = 1 + cbet2 981 | alp12 = 2 * math.atan2( somg12 * ( sbet1 * dbet2 + sbet2 * dbet1 ), 982 | domg12 * ( sbet1 * sbet2 + dbet1 * dbet2 ) ) 983 | else: 984 | # alp12 = alp2 - alp1, used in atan2 so no need to normalize 985 | salp12 = salp2 * calp1 - calp2 * salp1 986 | calp12 = calp2 * calp1 + salp2 * salp1 987 | # The right thing appears to happen if alp1 = +/-180 and alp2 = 0, viz 988 | # salp12 = -0 and alp12 = -180. However this depends on the sign 989 | # being attached to 0 correctly. The following ensures the correct 990 | # behavior. 991 | if salp12 == 0 and calp12 < 0: 992 | salp12 = Geodesic.tiny_ * calp1 993 | calp12 = -1.0 994 | alp12 = math.atan2(salp12, calp12) 995 | S12 += self._c2 * alp12 996 | S12 *= swapp * lonsign * latsign 997 | # Convert -0 to 0 998 | S12 += 0.0 999 | 1000 | # Convert calp, salp to azimuth accounting for lonsign, swapp, latsign. 1001 | if swapp < 0: 1002 | salp2, salp1 = salp1, salp2 1003 | calp2, calp1 = calp1, calp2 1004 | if outmask & Geodesic.GEODESICSCALE: 1005 | M21, M12 = M12, M21 1006 | 1007 | salp1 *= swapp * lonsign; calp1 *= swapp * latsign 1008 | salp2 *= swapp * lonsign; calp2 *= swapp * latsign 1009 | 1010 | return a12, s12, salp1, calp1, salp2, calp2, m12, M12, M21, S12 1011 | 1012 | def Inverse(self, lat1, lon1, lat2, lon2, 1013 | outmask = GeodesicCapability.STANDARD): 1014 | """Solve the inverse geodesic problem 1015 | 1016 | :param lat1: latitude of the first point in degrees 1017 | :param lon1: longitude of the first point in degrees 1018 | :param lat2: latitude of the second point in degrees 1019 | :param lon2: longitude of the second point in degrees 1020 | :param outmask: the :ref:`output mask ` 1021 | :return: a :ref:`dict` 1022 | 1023 | Compute geodesic between (*lat1*, *lon1*) and (*lat2*, *lon2*). 1024 | The default value of *outmask* is STANDARD, i.e., the *lat1*, 1025 | *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are 1026 | returned. 1027 | 1028 | """ 1029 | 1030 | a12, s12, salp1,calp1, salp2,calp2, m12, M12, M21, S12 = self._GenInverse( 1031 | lat1, lon1, lat2, lon2, outmask) 1032 | outmask &= Geodesic.OUT_MASK 1033 | if outmask & Geodesic.LONG_UNROLL: 1034 | lon12, e = Math.AngDiff(lon1, lon2) 1035 | lon2 = (lon1 + lon12) + e 1036 | else: 1037 | lon2 = Math.AngNormalize(lon2) 1038 | result = {'lat1': Math.LatFix(lat1), 1039 | 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else 1040 | Math.AngNormalize(lon1), 1041 | 'lat2': Math.LatFix(lat2), 1042 | 'lon2': lon2} 1043 | result['a12'] = a12 1044 | if outmask & Geodesic.DISTANCE: result['s12'] = s12 1045 | if outmask & Geodesic.AZIMUTH: 1046 | result['azi1'] = Math.atan2d(salp1, calp1) 1047 | result['azi2'] = Math.atan2d(salp2, calp2) 1048 | if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 1049 | if outmask & Geodesic.GEODESICSCALE: 1050 | result['M12'] = M12; result['M21'] = M21 1051 | if outmask & Geodesic.AREA: result['S12'] = S12 1052 | return result 1053 | 1054 | # return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 1055 | def _GenDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask): 1056 | """Private: General version of direct problem""" 1057 | from geographiclib.geodesicline import GeodesicLine 1058 | # Automatically supply DISTANCE_IN if necessary 1059 | if not arcmode: outmask |= Geodesic.DISTANCE_IN 1060 | line = GeodesicLine(self, lat1, lon1, azi1, outmask) 1061 | return line._GenPosition(arcmode, s12_a12, outmask) 1062 | 1063 | def Direct(self, lat1, lon1, azi1, s12, 1064 | outmask = GeodesicCapability.STANDARD): 1065 | """Solve the direct geodesic problem 1066 | 1067 | :param lat1: latitude of the first point in degrees 1068 | :param lon1: longitude of the first point in degrees 1069 | :param azi1: azimuth at the first point in degrees 1070 | :param s12: the distance from the first point to the second in 1071 | meters 1072 | :param outmask: the :ref:`output mask ` 1073 | :return: a :ref:`dict` 1074 | 1075 | Compute geodesic starting at (*lat1*, *lon1*) with azimuth *azi1* 1076 | and length *s12*. The default value of *outmask* is STANDARD, i.e., 1077 | the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* 1078 | entries are returned. 1079 | 1080 | """ 1081 | 1082 | a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenDirect( 1083 | lat1, lon1, azi1, False, s12, outmask) 1084 | outmask &= Geodesic.OUT_MASK 1085 | result = {'lat1': Math.LatFix(lat1), 1086 | 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else 1087 | Math.AngNormalize(lon1), 1088 | 'azi1': Math.AngNormalize(azi1), 1089 | 's12': s12} 1090 | result['a12'] = a12 1091 | if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 1092 | if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 1093 | if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 1094 | if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 1095 | if outmask & Geodesic.GEODESICSCALE: 1096 | result['M12'] = M12; result['M21'] = M21 1097 | if outmask & Geodesic.AREA: result['S12'] = S12 1098 | return result 1099 | 1100 | def ArcDirect(self, lat1, lon1, azi1, a12, 1101 | outmask = GeodesicCapability.STANDARD): 1102 | """Solve the direct geodesic problem in terms of spherical arc length 1103 | 1104 | :param lat1: latitude of the first point in degrees 1105 | :param lon1: longitude of the first point in degrees 1106 | :param azi1: azimuth at the first point in degrees 1107 | :param a12: spherical arc length from the first point to the second 1108 | in degrees 1109 | :param outmask: the :ref:`output mask ` 1110 | :return: a :ref:`dict` 1111 | 1112 | Compute geodesic starting at (*lat1*, *lon1*) with azimuth *azi1* 1113 | and arc length *a12*. The default value of *outmask* is STANDARD, 1114 | i.e., the *lat1*, *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, 1115 | *a12* entries are returned. 1116 | 1117 | """ 1118 | 1119 | a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenDirect( 1120 | lat1, lon1, azi1, True, a12, outmask) 1121 | outmask &= Geodesic.OUT_MASK 1122 | result = {'lat1': Math.LatFix(lat1), 1123 | 'lon1': lon1 if outmask & Geodesic.LONG_UNROLL else 1124 | Math.AngNormalize(lon1), 1125 | 'azi1': Math.AngNormalize(azi1), 1126 | 'a12': a12} 1127 | if outmask & Geodesic.DISTANCE: result['s12'] = s12 1128 | if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 1129 | if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 1130 | if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 1131 | if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 1132 | if outmask & Geodesic.GEODESICSCALE: 1133 | result['M12'] = M12; result['M21'] = M21 1134 | if outmask & Geodesic.AREA: result['S12'] = S12 1135 | return result 1136 | 1137 | def Line(self, lat1, lon1, azi1, 1138 | caps = GeodesicCapability.STANDARD | 1139 | GeodesicCapability.DISTANCE_IN): 1140 | """Return a GeodesicLine object 1141 | 1142 | :param lat1: latitude of the first point in degrees 1143 | :param lon1: longitude of the first point in degrees 1144 | :param azi1: azimuth at the first point in degrees 1145 | :param caps: the :ref:`capabilities ` 1146 | :return: a :class:`~geographiclib.geodesicline.GeodesicLine` 1147 | 1148 | This allows points along a geodesic starting at (*lat1*, *lon1*), 1149 | with azimuth *azi1* to be found. The default value of *caps* is 1150 | STANDARD | DISTANCE_IN, allowing direct geodesic problem to be 1151 | solved. 1152 | 1153 | """ 1154 | 1155 | from geographiclib.geodesicline import GeodesicLine 1156 | return GeodesicLine(self, lat1, lon1, azi1, caps) 1157 | 1158 | def _GenDirectLine(self, lat1, lon1, azi1, arcmode, s12_a12, 1159 | caps = GeodesicCapability.STANDARD | 1160 | GeodesicCapability.DISTANCE_IN): 1161 | """Private: general form of DirectLine""" 1162 | from geographiclib.geodesicline import GeodesicLine 1163 | # Automatically supply DISTANCE_IN if necessary 1164 | if not arcmode: caps |= Geodesic.DISTANCE_IN 1165 | line = GeodesicLine(self, lat1, lon1, azi1, caps) 1166 | if arcmode: 1167 | line.SetArc(s12_a12) 1168 | else: 1169 | line.SetDistance(s12_a12) 1170 | return line 1171 | 1172 | def DirectLine(self, lat1, lon1, azi1, s12, 1173 | caps = GeodesicCapability.STANDARD | 1174 | GeodesicCapability.DISTANCE_IN): 1175 | """Define a GeodesicLine object in terms of the direct geodesic 1176 | problem specified in terms of spherical arc length 1177 | 1178 | :param lat1: latitude of the first point in degrees 1179 | :param lon1: longitude of the first point in degrees 1180 | :param azi1: azimuth at the first point in degrees 1181 | :param s12: the distance from the first point to the second in 1182 | meters 1183 | :param caps: the :ref:`capabilities ` 1184 | :return: a :class:`~geographiclib.geodesicline.GeodesicLine` 1185 | 1186 | This function sets point 3 of the GeodesicLine to correspond to 1187 | point 2 of the direct geodesic problem. The default value of *caps* 1188 | is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be 1189 | solved. 1190 | 1191 | """ 1192 | 1193 | return self._GenDirectLine(lat1, lon1, azi1, False, s12, caps) 1194 | 1195 | def ArcDirectLine(self, lat1, lon1, azi1, a12, 1196 | caps = GeodesicCapability.STANDARD | 1197 | GeodesicCapability.DISTANCE_IN): 1198 | """Define a GeodesicLine object in terms of the direct geodesic 1199 | problem specified in terms of spherical arc length 1200 | 1201 | :param lat1: latitude of the first point in degrees 1202 | :param lon1: longitude of the first point in degrees 1203 | :param azi1: azimuth at the first point in degrees 1204 | :param a12: spherical arc length from the first point to the second 1205 | in degrees 1206 | :param caps: the :ref:`capabilities ` 1207 | :return: a :class:`~geographiclib.geodesicline.GeodesicLine` 1208 | 1209 | This function sets point 3 of the GeodesicLine to correspond to 1210 | point 2 of the direct geodesic problem. The default value of *caps* 1211 | is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be 1212 | solved. 1213 | 1214 | """ 1215 | 1216 | return self._GenDirectLine(lat1, lon1, azi1, True, a12, caps) 1217 | 1218 | def InverseLine(self, lat1, lon1, lat2, lon2, 1219 | caps = GeodesicCapability.STANDARD | 1220 | GeodesicCapability.DISTANCE_IN): 1221 | """Define a GeodesicLine object in terms of the invese geodesic problem 1222 | 1223 | :param lat1: latitude of the first point in degrees 1224 | :param lon1: longitude of the first point in degrees 1225 | :param lat2: latitude of the second point in degrees 1226 | :param lon2: longitude of the second point in degrees 1227 | :param caps: the :ref:`capabilities ` 1228 | :return: a :class:`~geographiclib.geodesicline.GeodesicLine` 1229 | 1230 | This function sets point 3 of the GeodesicLine to correspond to 1231 | point 2 of the inverse geodesic problem. The default value of *caps* 1232 | is STANDARD | DISTANCE_IN, allowing direct geodesic problem to be 1233 | solved. 1234 | 1235 | """ 1236 | 1237 | from geographiclib.geodesicline import GeodesicLine 1238 | a12, _, salp1, calp1, _, _, _, _, _, _ = self._GenInverse( 1239 | lat1, lon1, lat2, lon2, 0) 1240 | azi1 = Math.atan2d(salp1, calp1) 1241 | if caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN): 1242 | caps |= Geodesic.DISTANCE 1243 | line = GeodesicLine(self, lat1, lon1, azi1, caps, salp1, calp1) 1244 | line.SetArc(a12) 1245 | return line 1246 | 1247 | def Polygon(self, polyline = False): 1248 | """Return a PolygonArea object 1249 | 1250 | :param polyline: if True then the object describes a polyline 1251 | instead of a polygon 1252 | :return: a :class:`~geographiclib.polygonarea.PolygonArea` 1253 | 1254 | """ 1255 | 1256 | from geographiclib.polygonarea import PolygonArea 1257 | return PolygonArea(self, polyline) 1258 | 1259 | EMPTY = GeodesicCapability.EMPTY 1260 | """No capabilities, no output.""" 1261 | LATITUDE = GeodesicCapability.LATITUDE 1262 | """Calculate latitude *lat2*.""" 1263 | LONGITUDE = GeodesicCapability.LONGITUDE 1264 | """Calculate longitude *lon2*.""" 1265 | AZIMUTH = GeodesicCapability.AZIMUTH 1266 | """Calculate azimuths *azi1* and *azi2*.""" 1267 | DISTANCE = GeodesicCapability.DISTANCE 1268 | """Calculate distance *s12*.""" 1269 | STANDARD = GeodesicCapability.STANDARD 1270 | """All of the above.""" 1271 | DISTANCE_IN = GeodesicCapability.DISTANCE_IN 1272 | """Allow distance *s12* to be used as input in the direct geodesic 1273 | problem.""" 1274 | REDUCEDLENGTH = GeodesicCapability.REDUCEDLENGTH 1275 | """Calculate reduced length *m12*.""" 1276 | GEODESICSCALE = GeodesicCapability.GEODESICSCALE 1277 | """Calculate geodesic scales *M12* and *M21*.""" 1278 | AREA = GeodesicCapability.AREA 1279 | """Calculate area *S12*.""" 1280 | ALL = GeodesicCapability.ALL 1281 | """All of the above.""" 1282 | LONG_UNROLL = GeodesicCapability.LONG_UNROLL 1283 | """Unroll longitudes, rather than reducing them to the range 1284 | [-180d,180d]. 1285 | 1286 | """ 1287 | 1288 | Geodesic.WGS84 = Geodesic(Constants.WGS84_a, Constants.WGS84_f) 1289 | """Instantiation for the WGS84 ellipsoid""" 1290 | -------------------------------------------------------------------------------- /geographiclib/geodesiccapability.py: -------------------------------------------------------------------------------- 1 | """geodesiccapability.py: capability constants for geodesic{,line}.py""" 2 | # geodesiccapability.py 3 | # 4 | # This gathers the capability constants need by geodesic.py and 5 | # geodesicline.py. See the documentation for the GeographicLib::Geodesic class 6 | # for more information at 7 | # 8 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 9 | # 10 | # Copyright (c) Charles Karney (2011-2022) and licensed 11 | # under the MIT/X11 License. For more information, see 12 | # https://geographiclib.sourceforge.io/ 13 | ###################################################################### 14 | 15 | class GeodesicCapability: 16 | """ 17 | Capability constants shared between Geodesic and GeodesicLine. 18 | """ 19 | 20 | CAP_NONE = 0 21 | CAP_C1 = 1 << 0 22 | CAP_C1p = 1 << 1 23 | CAP_C2 = 1 << 2 24 | CAP_C3 = 1 << 3 25 | CAP_C4 = 1 << 4 26 | CAP_ALL = 0x1F 27 | CAP_MASK = CAP_ALL 28 | OUT_ALL = 0x7F80 29 | OUT_MASK = 0xFF80 # Includes LONG_UNROLL 30 | EMPTY = 0 31 | LATITUDE = 1 << 7 | CAP_NONE 32 | LONGITUDE = 1 << 8 | CAP_C3 33 | AZIMUTH = 1 << 9 | CAP_NONE 34 | DISTANCE = 1 << 10 | CAP_C1 35 | STANDARD = LATITUDE | LONGITUDE | AZIMUTH | DISTANCE 36 | DISTANCE_IN = 1 << 11 | CAP_C1 | CAP_C1p 37 | REDUCEDLENGTH = 1 << 12 | CAP_C1 | CAP_C2 38 | GEODESICSCALE = 1 << 13 | CAP_C1 | CAP_C2 39 | AREA = 1 << 14 | CAP_C4 40 | LONG_UNROLL = 1 << 15 41 | ALL = OUT_ALL | CAP_ALL # Does not include LONG_UNROLL 42 | -------------------------------------------------------------------------------- /geographiclib/geodesicline.py: -------------------------------------------------------------------------------- 1 | """Define the :class:`~geographiclib.geodesicline.GeodesicLine` class 2 | 3 | The constructor defines the starting point of the line. Points on the 4 | line are given by 5 | 6 | * :meth:`~geographiclib.geodesicline.GeodesicLine.Position` position 7 | given in terms of distance 8 | * :meth:`~geographiclib.geodesicline.GeodesicLine.ArcPosition` position 9 | given in terms of spherical arc length 10 | 11 | A reference point 3 can be defined with 12 | 13 | * :meth:`~geographiclib.geodesicline.GeodesicLine.SetDistance` set 14 | position of 3 in terms of the distance from the starting point 15 | * :meth:`~geographiclib.geodesicline.GeodesicLine.SetArc` set 16 | position of 3 in terms of the spherical arc length from the starting point 17 | 18 | The object can also be constructed by 19 | 20 | * :meth:`Geodesic.Line ` 21 | * :meth:`Geodesic.DirectLine ` 22 | * :meth:`Geodesic.ArcDirectLine 23 | ` 24 | * :meth:`Geodesic.InverseLine ` 25 | 26 | The public attributes for this class are 27 | 28 | * :attr:`~geographiclib.geodesicline.GeodesicLine.a` 29 | :attr:`~geographiclib.geodesicline.GeodesicLine.f` 30 | :attr:`~geographiclib.geodesicline.GeodesicLine.caps` 31 | :attr:`~geographiclib.geodesicline.GeodesicLine.lat1` 32 | :attr:`~geographiclib.geodesicline.GeodesicLine.lon1` 33 | :attr:`~geographiclib.geodesicline.GeodesicLine.azi1` 34 | :attr:`~geographiclib.geodesicline.GeodesicLine.salp1` 35 | :attr:`~geographiclib.geodesicline.GeodesicLine.calp1` 36 | :attr:`~geographiclib.geodesicline.GeodesicLine.s13` 37 | :attr:`~geographiclib.geodesicline.GeodesicLine.a13` 38 | 39 | """ 40 | # geodesicline.py 41 | # 42 | # This is a rather literal translation of the GeographicLib::GeodesicLine class 43 | # to python. See the documentation for the C++ class for more information at 44 | # 45 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 46 | # 47 | # The algorithms are derived in 48 | # 49 | # Charles F. F. Karney, 50 | # Algorithms for geodesics, J. Geodesy 87, 43-55 (2013), 51 | # https://doi.org/10.1007/s00190-012-0578-z 52 | # Addenda: https://geographiclib.sourceforge.io/geod-addenda.html 53 | # 54 | # Copyright (c) Charles Karney (2011-2022) and licensed 55 | # under the MIT/X11 License. For more information, see 56 | # https://geographiclib.sourceforge.io/ 57 | ###################################################################### 58 | 59 | import math 60 | from geographiclib.geomath import Math 61 | from geographiclib.geodesiccapability import GeodesicCapability 62 | 63 | class GeodesicLine: 64 | """Points on a geodesic path""" 65 | 66 | def __init__(self, geod, lat1, lon1, azi1, 67 | caps = GeodesicCapability.STANDARD | 68 | GeodesicCapability.DISTANCE_IN, 69 | salp1 = math.nan, calp1 = math.nan): 70 | """Construct a GeodesicLine object 71 | 72 | :param geod: a :class:`~geographiclib.geodesic.Geodesic` object 73 | :param lat1: latitude of the first point in degrees 74 | :param lon1: longitude of the first point in degrees 75 | :param azi1: azimuth at the first point in degrees 76 | :param caps: the :ref:`capabilities ` 77 | 78 | This creates an object allowing points along a geodesic starting at 79 | (*lat1*, *lon1*), with azimuth *azi1* to be found. The default 80 | value of *caps* is STANDARD | DISTANCE_IN. The optional parameters 81 | *salp1* and *calp1* should not be supplied; they are part of the 82 | private interface. 83 | 84 | """ 85 | 86 | from geographiclib.geodesic import Geodesic 87 | self.a = geod.a 88 | """The equatorial radius in meters (readonly)""" 89 | self.f = geod.f 90 | """The flattening (readonly)""" 91 | self._b = geod._b 92 | self._c2 = geod._c2 93 | self._f1 = geod._f1 94 | self.caps = (caps | Geodesic.LATITUDE | Geodesic.AZIMUTH | 95 | Geodesic.LONG_UNROLL) 96 | """the capabilities (readonly)""" 97 | 98 | # Guard against underflow in salp0 99 | self.lat1 = Math.LatFix(lat1) 100 | """the latitude of the first point in degrees (readonly)""" 101 | self.lon1 = lon1 102 | """the longitude of the first point in degrees (readonly)""" 103 | if math.isnan(salp1) or math.isnan(calp1): 104 | self.azi1 = Math.AngNormalize(azi1) 105 | self.salp1, self.calp1 = Math.sincosd(Math.AngRound(azi1)) 106 | else: 107 | self.azi1 = azi1 108 | """the azimuth at the first point in degrees (readonly)""" 109 | self.salp1 = salp1 110 | """the sine of the azimuth at the first point (readonly)""" 111 | self.calp1 = calp1 112 | """the cosine of the azimuth at the first point (readonly)""" 113 | 114 | # real cbet1, sbet1 115 | sbet1, cbet1 = Math.sincosd(Math.AngRound(self.lat1)); sbet1 *= self._f1 116 | # Ensure cbet1 = +epsilon at poles 117 | sbet1, cbet1 = Math.norm(sbet1, cbet1); cbet1 = max(Geodesic.tiny_, cbet1) 118 | self._dn1 = math.sqrt(1 + geod._ep2 * Math.sq(sbet1)) 119 | 120 | # Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0), 121 | self._salp0 = self.salp1 * cbet1 # alp0 in [0, pi/2 - |bet1|] 122 | # Alt: calp0 = hypot(sbet1, calp1 * cbet1). The following 123 | # is slightly better (consider the case salp1 = 0). 124 | self._calp0 = math.hypot(self.calp1, self.salp1 * sbet1) 125 | # Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1). 126 | # sig = 0 is nearest northward crossing of equator. 127 | # With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line). 128 | # With bet1 = pi/2, alp1 = -pi, sig1 = pi/2 129 | # With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2 130 | # Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1). 131 | # With alp0 in (0, pi/2], quadrants for sig and omg coincide. 132 | # No atan2(0,0) ambiguity at poles since cbet1 = +epsilon. 133 | # With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi. 134 | self._ssig1 = sbet1; self._somg1 = self._salp0 * sbet1 135 | self._csig1 = self._comg1 = (cbet1 * self.calp1 136 | if sbet1 != 0 or self.calp1 != 0 else 1) 137 | # sig1 in (-pi, pi] 138 | self._ssig1, self._csig1 = Math.norm(self._ssig1, self._csig1) 139 | # No need to normalize 140 | # self._somg1, self._comg1 = Math.norm(self._somg1, self._comg1) 141 | 142 | self._k2 = Math.sq(self._calp0) * geod._ep2 143 | eps = self._k2 / (2 * (1 + math.sqrt(1 + self._k2)) + self._k2) 144 | 145 | if self.caps & Geodesic.CAP_C1: 146 | self._A1m1 = Geodesic._A1m1f(eps) 147 | self._C1a = list(range(Geodesic.nC1_ + 1)) 148 | Geodesic._C1f(eps, self._C1a) 149 | self._B11 = Geodesic._SinCosSeries( 150 | True, self._ssig1, self._csig1, self._C1a) 151 | s = math.sin(self._B11); c = math.cos(self._B11) 152 | # tau1 = sig1 + B11 153 | self._stau1 = self._ssig1 * c + self._csig1 * s 154 | self._ctau1 = self._csig1 * c - self._ssig1 * s 155 | # Not necessary because C1pa reverts C1a 156 | # _B11 = -_SinCosSeries(true, _stau1, _ctau1, _C1pa) 157 | 158 | if self.caps & Geodesic.CAP_C1p: 159 | self._C1pa = list(range(Geodesic.nC1p_ + 1)) 160 | Geodesic._C1pf(eps, self._C1pa) 161 | 162 | if self.caps & Geodesic.CAP_C2: 163 | self._A2m1 = Geodesic._A2m1f(eps) 164 | self._C2a = list(range(Geodesic.nC2_ + 1)) 165 | Geodesic._C2f(eps, self._C2a) 166 | self._B21 = Geodesic._SinCosSeries( 167 | True, self._ssig1, self._csig1, self._C2a) 168 | 169 | if self.caps & Geodesic.CAP_C3: 170 | self._C3a = list(range(Geodesic.nC3_)) 171 | geod._C3f(eps, self._C3a) 172 | self._A3c = -self.f * self._salp0 * geod._A3f(eps) 173 | self._B31 = Geodesic._SinCosSeries( 174 | True, self._ssig1, self._csig1, self._C3a) 175 | 176 | if self.caps & Geodesic.CAP_C4: 177 | self._C4a = list(range(Geodesic.nC4_)) 178 | geod._C4f(eps, self._C4a) 179 | # Multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0) 180 | self._A4 = Math.sq(self.a) * self._calp0 * self._salp0 * geod._e2 181 | self._B41 = Geodesic._SinCosSeries( 182 | False, self._ssig1, self._csig1, self._C4a) 183 | self.s13 = math.nan 184 | """the distance between point 1 and point 3 in meters (readonly)""" 185 | self.a13 = math.nan 186 | """the arc length between point 1 and point 3 in degrees (readonly)""" 187 | 188 | # return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 189 | def _GenPosition(self, arcmode, s12_a12, outmask): 190 | """Private: General solution of position along geodesic""" 191 | from geographiclib.geodesic import Geodesic 192 | a12 = lat2 = lon2 = azi2 = s12 = m12 = M12 = M21 = S12 = math.nan 193 | outmask &= self.caps & Geodesic.OUT_MASK 194 | if not (arcmode or 195 | (self.caps & (Geodesic.OUT_MASK & Geodesic.DISTANCE_IN))): 196 | # Uninitialized or impossible distance calculation requested 197 | return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 198 | 199 | # Avoid warning about uninitialized B12. 200 | B12 = 0.0; AB1 = 0.0 201 | if arcmode: 202 | # Interpret s12_a12 as spherical arc length 203 | sig12 = math.radians(s12_a12) 204 | ssig12, csig12 = Math.sincosd(s12_a12) 205 | else: 206 | # Interpret s12_a12 as distance 207 | tau12 = s12_a12 / (self._b * (1 + self._A1m1)) 208 | tau12 = tau12 if math.isfinite(tau12) else math.nan 209 | s = math.sin(tau12); c = math.cos(tau12) 210 | # tau2 = tau1 + tau12 211 | B12 = - Geodesic._SinCosSeries(True, 212 | self._stau1 * c + self._ctau1 * s, 213 | self._ctau1 * c - self._stau1 * s, 214 | self._C1pa) 215 | sig12 = tau12 - (B12 - self._B11) 216 | ssig12 = math.sin(sig12); csig12 = math.cos(sig12) 217 | if abs(self.f) > 0.01: 218 | # Reverted distance series is inaccurate for |f| > 1/100, so correct 219 | # sig12 with 1 Newton iteration. The following table shows the 220 | # approximate maximum error for a = WGS_a() and various f relative to 221 | # GeodesicExact. 222 | # erri = the error in the inverse solution (nm) 223 | # errd = the error in the direct solution (series only) (nm) 224 | # errda = the error in the direct solution (series + 1 Newton) (nm) 225 | # 226 | # f erri errd errda 227 | # -1/5 12e6 1.2e9 69e6 228 | # -1/10 123e3 12e6 765e3 229 | # -1/20 1110 108e3 7155 230 | # -1/50 18.63 200.9 27.12 231 | # -1/100 18.63 23.78 23.37 232 | # -1/150 18.63 21.05 20.26 233 | # 1/150 22.35 24.73 25.83 234 | # 1/100 22.35 25.03 25.31 235 | # 1/50 29.80 231.9 30.44 236 | # 1/20 5376 146e3 10e3 237 | # 1/10 829e3 22e6 1.5e6 238 | # 1/5 157e6 3.8e9 280e6 239 | ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12 240 | csig2 = self._csig1 * csig12 - self._ssig1 * ssig12 241 | B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a) 242 | serr = ((1 + self._A1m1) * (sig12 + (B12 - self._B11)) - 243 | s12_a12 / self._b) 244 | sig12 = sig12 - serr / math.sqrt(1 + self._k2 * Math.sq(ssig2)) 245 | ssig12 = math.sin(sig12); csig12 = math.cos(sig12) 246 | # Update B12 below 247 | 248 | # real omg12, lam12, lon12 249 | # real ssig2, csig2, sbet2, cbet2, somg2, comg2, salp2, calp2 250 | # sig2 = sig1 + sig12 251 | ssig2 = self._ssig1 * csig12 + self._csig1 * ssig12 252 | csig2 = self._csig1 * csig12 - self._ssig1 * ssig12 253 | dn2 = math.sqrt(1 + self._k2 * Math.sq(ssig2)) 254 | if outmask & ( 255 | Geodesic.DISTANCE | Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): 256 | if arcmode or abs(self.f) > 0.01: 257 | B12 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C1a) 258 | AB1 = (1 + self._A1m1) * (B12 - self._B11) 259 | # sin(bet2) = cos(alp0) * sin(sig2) 260 | sbet2 = self._calp0 * ssig2 261 | # Alt: cbet2 = hypot(csig2, salp0 * ssig2) 262 | cbet2 = math.hypot(self._salp0, self._calp0 * csig2) 263 | if cbet2 == 0: 264 | # I.e., salp0 = 0, csig2 = 0. Break the degeneracy in this case 265 | cbet2 = csig2 = Geodesic.tiny_ 266 | # tan(alp0) = cos(sig2)*tan(alp2) 267 | salp2 = self._salp0; calp2 = self._calp0 * csig2 # No need to normalize 268 | 269 | if outmask & Geodesic.DISTANCE: 270 | s12 = self._b * ((1 + self._A1m1) * sig12 + AB1) if arcmode else s12_a12 271 | 272 | if outmask & Geodesic.LONGITUDE: 273 | # tan(omg2) = sin(alp0) * tan(sig2) 274 | somg2 = self._salp0 * ssig2; comg2 = csig2 # No need to normalize 275 | E = math.copysign(1, self._salp0) # East or west going? 276 | # omg12 = omg2 - omg1 277 | omg12 = (E * (sig12 278 | - (math.atan2( ssig2, csig2) - 279 | math.atan2( self._ssig1, self._csig1)) 280 | + (math.atan2(E * somg2, comg2) - 281 | math.atan2(E * self._somg1, self._comg1))) 282 | if outmask & Geodesic.LONG_UNROLL 283 | else math.atan2(somg2 * self._comg1 - comg2 * self._somg1, 284 | comg2 * self._comg1 + somg2 * self._somg1)) 285 | lam12 = omg12 + self._A3c * ( 286 | sig12 + (Geodesic._SinCosSeries(True, ssig2, csig2, self._C3a) 287 | - self._B31)) 288 | lon12 = math.degrees(lam12) 289 | lon2 = (self.lon1 + lon12 if outmask & Geodesic.LONG_UNROLL else 290 | Math.AngNormalize(Math.AngNormalize(self.lon1) + 291 | Math.AngNormalize(lon12))) 292 | 293 | if outmask & Geodesic.LATITUDE: 294 | lat2 = Math.atan2d(sbet2, self._f1 * cbet2) 295 | 296 | if outmask & Geodesic.AZIMUTH: 297 | azi2 = Math.atan2d(salp2, calp2) 298 | 299 | if outmask & (Geodesic.REDUCEDLENGTH | Geodesic.GEODESICSCALE): 300 | B22 = Geodesic._SinCosSeries(True, ssig2, csig2, self._C2a) 301 | AB2 = (1 + self._A2m1) * (B22 - self._B21) 302 | J12 = (self._A1m1 - self._A2m1) * sig12 + (AB1 - AB2) 303 | if outmask & Geodesic.REDUCEDLENGTH: 304 | # Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure 305 | # accurate cancellation in the case of coincident points. 306 | m12 = self._b * (( dn2 * (self._csig1 * ssig2) - 307 | self._dn1 * (self._ssig1 * csig2)) 308 | - self._csig1 * csig2 * J12) 309 | if outmask & Geodesic.GEODESICSCALE: 310 | t = (self._k2 * (ssig2 - self._ssig1) * 311 | (ssig2 + self._ssig1) / (self._dn1 + dn2)) 312 | M12 = csig12 + (t * ssig2 - csig2 * J12) * self._ssig1 / self._dn1 313 | M21 = csig12 - (t * self._ssig1 - self._csig1 * J12) * ssig2 / dn2 314 | 315 | if outmask & Geodesic.AREA: 316 | B42 = Geodesic._SinCosSeries(False, ssig2, csig2, self._C4a) 317 | # real salp12, calp12 318 | if self._calp0 == 0 or self._salp0 == 0: 319 | # alp12 = alp2 - alp1, used in atan2 so no need to normalize 320 | salp12 = salp2 * self.calp1 - calp2 * self.salp1 321 | calp12 = calp2 * self.calp1 + salp2 * self.salp1 322 | else: 323 | # tan(alp) = tan(alp0) * sec(sig) 324 | # tan(alp2-alp1) = (tan(alp2) -tan(alp1)) / (tan(alp2)*tan(alp1)+1) 325 | # = calp0 * salp0 * (csig1-csig2) / (salp0^2 + calp0^2 * csig1*csig2) 326 | # If csig12 > 0, write 327 | # csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1) 328 | # else 329 | # csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1 330 | # No need to normalize 331 | salp12 = self._calp0 * self._salp0 * ( 332 | self._csig1 * (1 - csig12) + ssig12 * self._ssig1 if csig12 <= 0 333 | else ssig12 * (self._csig1 * ssig12 / (1 + csig12) + self._ssig1)) 334 | calp12 = (Math.sq(self._salp0) + 335 | Math.sq(self._calp0) * self._csig1 * csig2) 336 | S12 = (self._c2 * math.atan2(salp12, calp12) + 337 | self._A4 * (B42 - self._B41)) 338 | 339 | a12 = s12_a12 if arcmode else math.degrees(sig12) 340 | return a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 341 | 342 | def Position(self, s12, outmask = GeodesicCapability.STANDARD): 343 | """Find the position on the line given *s12* 344 | 345 | :param s12: the distance from the first point to the second in 346 | meters 347 | :param outmask: the :ref:`output mask ` 348 | :return: a :ref:`dict` 349 | 350 | The default value of *outmask* is STANDARD, i.e., the *lat1*, 351 | *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are 352 | returned. The :class:`~geographiclib.geodesicline.GeodesicLine` 353 | object must have been constructed with the DISTANCE_IN capability. 354 | 355 | """ 356 | 357 | from geographiclib.geodesic import Geodesic 358 | result = {'lat1': self.lat1, 359 | 'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else 360 | Math.AngNormalize(self.lon1), 361 | 'azi1': self.azi1, 's12': s12} 362 | a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition( 363 | False, s12, outmask) 364 | outmask &= Geodesic.OUT_MASK 365 | result['a12'] = a12 366 | if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 367 | if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 368 | if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 369 | if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 370 | if outmask & Geodesic.GEODESICSCALE: 371 | result['M12'] = M12; result['M21'] = M21 372 | if outmask & Geodesic.AREA: result['S12'] = S12 373 | return result 374 | 375 | def ArcPosition(self, a12, outmask = GeodesicCapability.STANDARD): 376 | """Find the position on the line given *a12* 377 | 378 | :param a12: spherical arc length from the first point to the second 379 | in degrees 380 | :param outmask: the :ref:`output mask ` 381 | :return: a :ref:`dict` 382 | 383 | The default value of *outmask* is STANDARD, i.e., the *lat1*, 384 | *lon1*, *azi1*, *lat2*, *lon2*, *azi2*, *s12*, *a12* entries are 385 | returned. 386 | 387 | """ 388 | 389 | from geographiclib.geodesic import Geodesic 390 | result = {'lat1': self.lat1, 391 | 'lon1': self.lon1 if outmask & Geodesic.LONG_UNROLL else 392 | Math.AngNormalize(self.lon1), 393 | 'azi1': self.azi1, 'a12': a12} 394 | a12, lat2, lon2, azi2, s12, m12, M12, M21, S12 = self._GenPosition( 395 | True, a12, outmask) 396 | outmask &= Geodesic.OUT_MASK 397 | if outmask & Geodesic.DISTANCE: result['s12'] = s12 398 | if outmask & Geodesic.LATITUDE: result['lat2'] = lat2 399 | if outmask & Geodesic.LONGITUDE: result['lon2'] = lon2 400 | if outmask & Geodesic.AZIMUTH: result['azi2'] = azi2 401 | if outmask & Geodesic.REDUCEDLENGTH: result['m12'] = m12 402 | if outmask & Geodesic.GEODESICSCALE: 403 | result['M12'] = M12; result['M21'] = M21 404 | if outmask & Geodesic.AREA: result['S12'] = S12 405 | return result 406 | 407 | def SetDistance(self, s13): 408 | """Specify the position of point 3 in terms of distance 409 | 410 | :param s13: distance from point 1 to point 3 in meters 411 | 412 | """ 413 | 414 | self.s13 = s13 415 | self.a13, _, _, _, _, _, _, _, _ = self._GenPosition(False, self.s13, 0) 416 | 417 | def SetArc(self, a13): 418 | """Specify the position of point 3 in terms of arc length 419 | 420 | :param a13: spherical arc length from point 1 to point 3 in degrees 421 | 422 | """ 423 | 424 | from geographiclib.geodesic import Geodesic 425 | self.a13 = a13 426 | _, _, _, _, self.s13, _, _, _, _ = self._GenPosition(True, self.a13, 427 | Geodesic.DISTANCE) 428 | -------------------------------------------------------------------------------- /geographiclib/geomath.py: -------------------------------------------------------------------------------- 1 | """geomath.py: transcription of GeographicLib::Math class.""" 2 | # geomath.py 3 | # 4 | # This is a rather literal translation of the GeographicLib::Math class to 5 | # python. See the documentation for the C++ class for more information at 6 | # 7 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 8 | # 9 | # Copyright (c) Charles Karney (2011-2022) and 10 | # licensed under the MIT/X11 License. For more information, see 11 | # https://geographiclib.sourceforge.io/ 12 | ###################################################################### 13 | 14 | import sys 15 | import math 16 | 17 | class Math: 18 | """ 19 | Additional math routines for GeographicLib. 20 | """ 21 | 22 | @staticmethod 23 | def sq(x): 24 | """Square a number""" 25 | 26 | return x * x 27 | 28 | @staticmethod 29 | def cbrt(x): 30 | """Real cube root of a number""" 31 | 32 | return math.copysign(math.pow(abs(x), 1/3.0), x) 33 | 34 | @staticmethod 35 | def norm(x, y): 36 | """Private: Normalize a two-vector.""" 37 | 38 | r = (math.sqrt(Math.sq(x) + Math.sq(y)) 39 | # hypot is inaccurate for 3.[89]. Problem reported by agdhruv 40 | # https://github.com/geopy/geopy/issues/466 ; see 41 | # https://bugs.python.org/issue43088 42 | # Visual Studio 2015 32-bit has a similar problem. 43 | if (3, 8) <= sys.version_info < (3, 10) 44 | else math.hypot(x, y)) 45 | return x/r, y/r 46 | 47 | @staticmethod 48 | def sum(u, v): 49 | """Error free transformation of a sum.""" 50 | 51 | # Error free transformation of a sum. Note that t can be the same as one 52 | # of the first two arguments. 53 | s = u + v 54 | up = s - v 55 | vpp = s - up 56 | up -= u 57 | vpp -= v 58 | t = s if s == 0 else 0.0 - (up + vpp) 59 | # u + v = s + t 60 | # = round(u + v) + t 61 | return s, t 62 | 63 | @staticmethod 64 | def polyval(N, p, s, x): 65 | """Evaluate a polynomial.""" 66 | 67 | y = float(0 if N < 0 else p[s]) # make sure the returned value is a float 68 | while N > 0: 69 | N -= 1; s += 1 70 | y = y * x + p[s] 71 | return y 72 | 73 | @staticmethod 74 | def AngRound(x): 75 | """Private: Round an angle so that small values underflow to zero.""" 76 | 77 | # The makes the smallest gap in x = 1/16 - nextafter(1/16, 0) = 1/2^57 78 | # for reals = 0.7 pm on the earth if x is an angle in degrees. (This 79 | # is about 1000 times more resolution than we get with angles around 90 80 | # degrees.) We use this to avoid having to deal with near singular 81 | # cases when x is non-zero but tiny (e.g., 1.0e-200). 82 | z = 1/16.0 83 | y = abs(x) 84 | # The compiler mustn't "simplify" z - (z - y) to y 85 | if y < z: y = z - (z - y) 86 | return math.copysign(y, x) 87 | 88 | @staticmethod 89 | def remainder(x, y): 90 | """remainder of x/y in the range [-y/2, y/2].""" 91 | 92 | return math.remainder(x, y) if math.isfinite(x) else math.nan 93 | 94 | @staticmethod 95 | def AngNormalize(x): 96 | """reduce angle to [-180,180]""" 97 | 98 | y = Math.remainder(x, 360) 99 | return math.copysign(180.0, x) if abs(y) == 180 else y 100 | 101 | @staticmethod 102 | def LatFix(x): 103 | """replace angles outside [-90,90] by NaN""" 104 | 105 | return math.nan if abs(x) > 90 else x 106 | 107 | @staticmethod 108 | def AngDiff(x, y): 109 | """compute y - x and reduce to [-180,180] accurately""" 110 | 111 | d, t = Math.sum(Math.remainder(-x, 360), Math.remainder(y, 360)) 112 | d, t = Math.sum(Math.remainder(d, 360), t) 113 | if d == 0 or abs(d) == 180: 114 | d = math.copysign(d, y - x if t == 0 else -t) 115 | return d, t 116 | 117 | @staticmethod 118 | def sincosd(x): 119 | """Compute sine and cosine of x in degrees.""" 120 | 121 | r = math.fmod(x, 360) if math.isfinite(x) else math.nan 122 | q = 0 if math.isnan(r) else int(round(r / 90)) 123 | r -= 90 * q; r = math.radians(r) 124 | s = math.sin(r); c = math.cos(r) 125 | q = q % 4 126 | if q == 1: s, c = c, -s 127 | elif q == 2: s, c = -s, -c 128 | elif q == 3: s, c = -c, s 129 | c = c + 0.0 130 | if s == 0: s = math.copysign(s, x) 131 | return s, c 132 | 133 | @staticmethod 134 | def sincosde(x, t): 135 | """Compute sine and cosine of (x + t) in degrees with x in [-180, 180]""" 136 | 137 | q = int(round(x / 90)) if math.isfinite(x) else 0 138 | r = x - 90 * q; r = math.radians(Math.AngRound(r + t)) 139 | s = math.sin(r); c = math.cos(r) 140 | q = q % 4 141 | if q == 1: s, c = c, -s 142 | elif q == 2: s, c = -s, -c 143 | elif q == 3: s, c = -c, s 144 | c = c + 0.0 145 | if s == 0: s = math.copysign(s, x) 146 | return s, c 147 | 148 | @staticmethod 149 | def atan2d(y, x): 150 | """compute atan2(y, x) with the result in degrees""" 151 | 152 | if abs(y) > abs(x): 153 | q = 2; x, y = y, x 154 | else: 155 | q = 0 156 | if x < 0: 157 | q += 1; x = -x 158 | ang = math.degrees(math.atan2(y, x)) 159 | if q == 1: ang = math.copysign(180, y) - ang 160 | elif q == 2: ang = 90 - ang 161 | elif q == 3: ang = -90 + ang 162 | return ang 163 | -------------------------------------------------------------------------------- /geographiclib/polygonarea.py: -------------------------------------------------------------------------------- 1 | """Define the :class:`~geographiclib.polygonarea.PolygonArea` class 2 | 3 | The constructor initializes a empty polygon. The available methods are 4 | 5 | * :meth:`~geographiclib.polygonarea.PolygonArea.Clear` reset the 6 | polygon 7 | * :meth:`~geographiclib.polygonarea.PolygonArea.AddPoint` add a vertex 8 | to the polygon 9 | * :meth:`~geographiclib.polygonarea.PolygonArea.AddEdge` add an edge 10 | to the polygon 11 | * :meth:`~geographiclib.polygonarea.PolygonArea.Compute` compute the 12 | properties of the polygon 13 | * :meth:`~geographiclib.polygonarea.PolygonArea.TestPoint` compute the 14 | properties of the polygon with a tentative additional vertex 15 | * :meth:`~geographiclib.polygonarea.PolygonArea.TestEdge` compute the 16 | properties of the polygon with a tentative additional edge 17 | 18 | The public attributes for this class are 19 | 20 | * :attr:`~geographiclib.polygonarea.PolygonArea.earth` 21 | :attr:`~geographiclib.polygonarea.PolygonArea.polyline` 22 | :attr:`~geographiclib.polygonarea.PolygonArea.area0` 23 | :attr:`~geographiclib.polygonarea.PolygonArea.num` 24 | :attr:`~geographiclib.polygonarea.PolygonArea.lat1` 25 | :attr:`~geographiclib.polygonarea.PolygonArea.lon1` 26 | 27 | """ 28 | # polygonarea.py 29 | # 30 | # This is a rather literal translation of the GeographicLib::PolygonArea class 31 | # to python. See the documentation for the C++ class for more information at 32 | # 33 | # https://geographiclib.sourceforge.io/C++/doc/annotated.html 34 | # 35 | # The algorithms are derived in 36 | # 37 | # Charles F. F. Karney, 38 | # Algorithms for geodesics, J. Geodesy 87, 43-55 (2013), 39 | # https://doi.org/10.1007/s00190-012-0578-z 40 | # Addenda: https://geographiclib.sourceforge.io/geod-addenda.html 41 | # 42 | # Copyright (c) Charles Karney (2011-2022) and licensed 43 | # under the MIT/X11 License. For more information, see 44 | # https://geographiclib.sourceforge.io/ 45 | ###################################################################### 46 | 47 | import math 48 | from geographiclib.geomath import Math 49 | from geographiclib.accumulator import Accumulator 50 | from geographiclib.geodesic import Geodesic 51 | 52 | class PolygonArea: 53 | """Area of a geodesic polygon""" 54 | 55 | @staticmethod 56 | def _transit(lon1, lon2): 57 | """Count crossings of prime meridian for AddPoint.""" 58 | # Return 1 or -1 if crossing prime meridian in east or west direction. 59 | # Otherwise return zero. 60 | # Compute lon12 the same way as Geodesic::Inverse. 61 | lon12, _ = Math.AngDiff(lon1, lon2) 62 | lon1 = Math.AngNormalize(lon1) 63 | lon2 = Math.AngNormalize(lon2) 64 | return (1 if lon12 > 0 and ( lon1 < 0 <= lon2 or 65 | (lon1 > 0 and lon2 == 0)) 66 | else (-1 if lon12 < 0 and lon2 < 0 <= lon1 else 0)) 67 | 68 | @staticmethod 69 | def _transitdirect(lon1, lon2): 70 | """Count crossings of prime meridian for AddEdge.""" 71 | # We want to compute exactly 72 | # int(floor(lon2 / 360)) - int(floor(lon1 / 360)) 73 | lon1 = Math.remainder(lon1, 720.0); lon2 = Math.remainder(lon2, 720.0) 74 | return ( (0 if 0 <= lon2 < 360 else 1) - 75 | (0 if 0 <= lon1 < 360 else 1) ) 76 | 77 | @staticmethod 78 | def _areareduceA(area, area0, crossings, reverse, sign): 79 | """Reduce accumulator area to allowed range.""" 80 | area.Remainder(area0) 81 | if crossings & 1: 82 | area.Add( (1 if area.Sum() < 0 else -1) * area0/2 ) 83 | # area is with the clockwise sense. If !reverse convert to 84 | # counter-clockwise convention. 85 | if not reverse: area.Negate() 86 | # If sign put area in (-area0/2, area0/2], else put area in [0, area0) 87 | if sign: 88 | if area.Sum() > area0/2: 89 | area.Add( -area0 ) 90 | elif area.Sum() <= -area0/2: 91 | area.Add( area0 ) 92 | else: 93 | if area.Sum() >= area0: 94 | area.Add( -area0 ) 95 | elif area.Sum() < 0: 96 | area.Add( area0 ) 97 | 98 | return 0.0 + area.Sum() 99 | 100 | @staticmethod 101 | def _areareduceB(area, area0, crossings, reverse, sign): 102 | """Reduce double area to allowed range.""" 103 | area = Math.remainder(area, area0) 104 | if crossings & 1: 105 | area += (1 if area < 0 else -1) * area0/2 106 | # area is with the clockwise sense. If !reverse convert to 107 | # counter-clockwise convention. 108 | if not reverse: area *= -1 109 | # If sign put area in (-area0/2, area0/2], else put area in [0, area0) 110 | if sign: 111 | if area > area0/2: 112 | area -= area0 113 | elif area <= -area0/2: 114 | area += area0 115 | else: 116 | if area >= area0: 117 | area -= area0 118 | elif area < 0: 119 | area += area0 120 | 121 | return 0.0 + area 122 | 123 | def __init__(self, earth, polyline = False): 124 | """Construct a PolygonArea object 125 | 126 | :param earth: a :class:`~geographiclib.geodesic.Geodesic` object 127 | :param polyline: if true, treat object as a polyline instead of a polygon 128 | 129 | Initially the polygon has no vertices. 130 | """ 131 | 132 | self.earth = earth 133 | """The geodesic object (readonly)""" 134 | self.polyline = polyline 135 | """Is this a polyline? (readonly)""" 136 | self.area0 = 4 * math.pi * earth._c2 137 | """The total area of the ellipsoid in meter^2 (readonly)""" 138 | self._mask = (Geodesic.LATITUDE | Geodesic.LONGITUDE | 139 | Geodesic.DISTANCE | 140 | (Geodesic.EMPTY if self.polyline else 141 | Geodesic.AREA | Geodesic.LONG_UNROLL)) 142 | if not self.polyline: self._areasum = Accumulator() 143 | self._perimetersum = Accumulator() 144 | self.num = 0 145 | """The current number of points in the polygon (readonly)""" 146 | self.lat1 = math.nan 147 | """The current latitude in degrees (readonly)""" 148 | self.lon1 = math.nan 149 | """The current longitude in degrees (readonly)""" 150 | self._crossings = 0 151 | self._lat0 = self._lon0 = math.nan 152 | 153 | def Clear(self): 154 | """Reset to empty polygon.""" 155 | self.num = 0 156 | self._crossings = 0 157 | if not self.polyline: self._areasum.Set(0) 158 | self._perimetersum.Set(0) 159 | self._lat0 = self._lon0 = self.lat1 = self.lon1 = math.nan 160 | 161 | def AddPoint(self, lat, lon): 162 | """Add the next vertex to the polygon 163 | 164 | :param lat: the latitude of the point in degrees 165 | :param lon: the longitude of the point in degrees 166 | 167 | This adds an edge from the current vertex to the new vertex. 168 | """ 169 | 170 | if self.num == 0: 171 | self._lat0 = self.lat1 = lat 172 | self._lon0 = self.lon1 = lon 173 | else: 174 | _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse( 175 | self.lat1, self.lon1, lat, lon, self._mask) 176 | self._perimetersum.Add(s12) 177 | if not self.polyline: 178 | self._areasum.Add(S12) 179 | self._crossings += PolygonArea._transit(self.lon1, lon) 180 | self.lat1 = lat 181 | self.lon1 = lon 182 | self.num += 1 183 | 184 | def AddEdge(self, azi, s): 185 | """Add the next edge to the polygon 186 | 187 | :param azi: the azimuth at the current the point in degrees 188 | :param s: the length of the edge in meters 189 | 190 | This specifies the new vertex in terms of the edge from the current 191 | vertex. 192 | 193 | """ 194 | 195 | if self.num != 0: 196 | _, lat, lon, _, _, _, _, _, S12 = self.earth._GenDirect( 197 | self.lat1, self.lon1, azi, False, s, self._mask) 198 | self._perimetersum.Add(s) 199 | if not self.polyline: 200 | self._areasum.Add(S12) 201 | self._crossings += PolygonArea._transitdirect(self.lon1, lon) 202 | self.lat1 = lat 203 | self.lon1 = lon 204 | self.num += 1 205 | 206 | # return number, perimeter, area 207 | def Compute(self, reverse = False, sign = True): 208 | """Compute the properties of the polygon 209 | 210 | :param reverse: if true then clockwise (instead of 211 | counter-clockwise) traversal counts as a positive area 212 | :param sign: if true then return a signed result for the area if the 213 | polygon is traversed in the "wrong" direction instead of returning 214 | the area for the rest of the earth 215 | :return: a tuple of number, perimeter (meters), area (meters^2) 216 | 217 | Arbitrarily complex polygons are allowed. In the case of 218 | self-intersecting polygons the area is accumulated "algebraically", 219 | e.g., the areas of the 2 loops in a figure-8 polygon will partially 220 | cancel. 221 | 222 | If the object is a polygon (and not a polyline), the perimeter 223 | includes the length of a final edge connecting the current point to 224 | the initial point. If the object is a polyline, then area is nan. 225 | 226 | More points can be added to the polygon after this call. 227 | 228 | """ 229 | if self.polyline: area = math.nan 230 | if self.num < 2: 231 | perimeter = 0.0 232 | if not self.polyline: area = 0.0 233 | return self.num, perimeter, area 234 | 235 | if self.polyline: 236 | perimeter = self._perimetersum.Sum() 237 | return self.num, perimeter, area 238 | 239 | _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse( 240 | self.lat1, self.lon1, self._lat0, self._lon0, self._mask) 241 | perimeter = self._perimetersum.Sum(s12) 242 | tempsum = Accumulator(self._areasum) 243 | tempsum.Add(S12) 244 | crossings = self._crossings + PolygonArea._transit(self.lon1, self._lon0) 245 | area = PolygonArea._areareduceA(tempsum, self.area0, crossings, 246 | reverse, sign) 247 | return self.num, perimeter, area 248 | 249 | # return number, perimeter, area 250 | def TestPoint(self, lat, lon, reverse = False, sign = True): 251 | """Compute the properties for a tentative additional vertex 252 | 253 | :param lat: the latitude of the point in degrees 254 | :param lon: the longitude of the point in degrees 255 | :param reverse: if true then clockwise (instead of 256 | counter-clockwise) traversal counts as a positive area 257 | :param sign: if true then return a signed result for the area if the 258 | polygon is traversed in the "wrong" direction instead of returning 259 | the area for the rest of the earth 260 | :return: a tuple of number, perimeter (meters), area (meters^2) 261 | 262 | """ 263 | if self.polyline: area = math.nan 264 | if self.num == 0: 265 | perimeter = 0.0 266 | if not self.polyline: area = 0.0 267 | return 1, perimeter, area 268 | 269 | perimeter = self._perimetersum.Sum() 270 | tempsum = 0.0 if self.polyline else self._areasum.Sum() 271 | crossings = self._crossings; num = self.num + 1 272 | for i in ([0] if self.polyline else [0, 1]): 273 | _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse( 274 | self.lat1 if i == 0 else lat, self.lon1 if i == 0 else lon, 275 | self._lat0 if i != 0 else lat, self._lon0 if i != 0 else lon, 276 | self._mask) 277 | perimeter += s12 278 | if not self.polyline: 279 | tempsum += S12 280 | crossings += PolygonArea._transit(self.lon1 if i == 0 else lon, 281 | self._lon0 if i != 0 else lon) 282 | 283 | if self.polyline: 284 | return num, perimeter, area 285 | 286 | area = PolygonArea._areareduceB(tempsum, self.area0, crossings, 287 | reverse, sign) 288 | return num, perimeter, area 289 | 290 | # return num, perimeter, area 291 | def TestEdge(self, azi, s, reverse = False, sign = True): 292 | """Compute the properties for a tentative additional edge 293 | 294 | :param azi: the azimuth at the current the point in degrees 295 | :param s: the length of the edge in meters 296 | :param reverse: if true then clockwise (instead of 297 | counter-clockwise) traversal counts as a positive area 298 | :param sign: if true then return a signed result for the area if the 299 | polygon is traversed in the "wrong" direction instead of returning 300 | the area for the rest of the earth 301 | :return: a tuple of number, perimeter (meters), area (meters^2) 302 | 303 | """ 304 | 305 | if self.num == 0: # we don't have a starting point! 306 | return 0, math.nan, math.nan 307 | num = self.num + 1 308 | perimeter = self._perimetersum.Sum() + s 309 | if self.polyline: 310 | return num, perimeter, math.nan 311 | 312 | tempsum = self._areasum.Sum() 313 | crossings = self._crossings 314 | _, lat, lon, _, _, _, _, _, S12 = self.earth._GenDirect( 315 | self.lat1, self.lon1, azi, False, s, self._mask) 316 | tempsum += S12 317 | crossings += PolygonArea._transitdirect(self.lon1, lon) 318 | _, s12, _, _, _, _, _, _, _, S12 = self.earth._GenInverse( 319 | lat, lon, self._lat0, self._lon0, self._mask) 320 | perimeter += s12 321 | tempsum += S12 322 | crossings += PolygonArea._transit(lon, self._lon0) 323 | 324 | area = PolygonArea._areareduceB(tempsum, self.area0, crossings, 325 | reverse, sign) 326 | return num, perimeter, area 327 | -------------------------------------------------------------------------------- /geographiclib/test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | test_geodesic: test the geodesic routines from GeographicLib 4 | 5 | Run these tests with one of 6 | 7 | python2 -m unittest -v geographiclib.test.test_geodesic 8 | python3 -m unittest -v geographiclib.test.test_geodesic 9 | 10 | executed in this directory's parent directory. 11 | 12 | """ 13 | -------------------------------------------------------------------------------- /geographiclib/test/test_geodesic.py: -------------------------------------------------------------------------------- 1 | """Geodesic tests""" 2 | 3 | import unittest 4 | import math 5 | 6 | from geographiclib.geodesic import Geodesic 7 | 8 | class GeodesicTest(unittest.TestCase): 9 | """Geodesic test suite""" 10 | 11 | testcases = [ 12 | [35.60777, -139.44815, 111.098748429560326, 13 | -11.17491, -69.95921, 129.289270889708762, 14 | 8935244.5604818305, 80.50729714281974, 6273170.2055303837, 15 | 0.16606318447386067, 0.16479116945612937, 12841384694976.432], 16 | [55.52454, 106.05087, 22.020059880982801, 17 | 77.03196, 197.18234, 109.112041110671519, 18 | 4105086.1713924406, 36.892740690445894, 3828869.3344387607, 19 | 0.80076349608092607, 0.80101006984201008, 61674961290615.615], 20 | [-21.97856, 142.59065, -32.44456876433189, 21 | 41.84138, 98.56635, -41.84359951440466, 22 | 8394328.894657671, 75.62930491011522, 6161154.5773110616, 23 | 0.24816339233950381, 0.24930251203627892, -6637997720646.717], 24 | [-66.99028, 112.2363, 173.73491240878403, 25 | -12.70631, 285.90344, 2.512956620913668, 26 | 11150344.2312080241, 100.278634181155759, 6289939.5670446687, 27 | -0.17199490274700385, -0.17722569526345708, -121287239862139.744], 28 | [-17.42761, 173.34268, -159.033557661192928, 29 | -15.84784, 5.93557, -20.787484651536988, 30 | 16076603.1631180673, 144.640108810286253, 3732902.1583877189, 31 | -0.81273638700070476, -0.81299800519154474, 97825992354058.708], 32 | [32.84994, 48.28919, 150.492927788121982, 33 | -56.28556, 202.29132, 48.113449399816759, 34 | 16727068.9438164461, 150.565799985466607, 3147838.1910180939, 35 | -0.87334918086923126, -0.86505036767110637, -72445258525585.010], 36 | [6.96833, 52.74123, 92.581585386317712, 37 | -7.39675, 206.17291, 90.721692165923907, 38 | 17102477.2496958388, 154.147366239113561, 2772035.6169917581, 39 | -0.89991282520302447, -0.89986892177110739, -1311796973197.995], 40 | [-50.56724, -16.30485, -105.439679907590164, 41 | -33.56571, -94.97412, -47.348547835650331, 42 | 6455670.5118668696, 58.083719495371259, 5409150.7979815838, 43 | 0.53053508035997263, 0.52988722644436602, 41071447902810.047], 44 | [-58.93002, -8.90775, 140.965397902500679, 45 | -8.91104, 133.13503, 19.255429433416599, 46 | 11756066.0219864627, 105.755691241406877, 6151101.2270708536, 47 | -0.26548622269867183, -0.27068483874510741, -86143460552774.735], 48 | [-68.82867, -74.28391, 93.774347763114881, 49 | -50.63005, -8.36685, 34.65564085411343, 50 | 3956936.926063544, 35.572254987389284, 3708890.9544062657, 51 | 0.81443963736383502, 0.81420859815358342, -41845309450093.787], 52 | [-10.62672, -32.0898, -86.426713286747751, 53 | 5.883, -134.31681, -80.473780971034875, 54 | 11470869.3864563009, 103.387395634504061, 6184411.6622659713, 55 | -0.23138683500430237, -0.23155097622286792, 4198803992123.548], 56 | [-21.76221, 166.90563, 29.319421206936428, 57 | 48.72884, 213.97627, 43.508671946410168, 58 | 9098627.3986554915, 81.963476716121964, 6299240.9166992283, 59 | 0.13965943368590333, 0.14152969707656796, 10024709850277.476], 60 | [-19.79938, -174.47484, 71.167275780171533, 61 | -11.99349, -154.35109, 65.589099775199228, 62 | 2319004.8601169389, 20.896611684802389, 2267960.8703918325, 63 | 0.93427001867125849, 0.93424887135032789, -3935477535005.785], 64 | [-11.95887, -116.94513, 92.712619830452549, 65 | 4.57352, 7.16501, 78.64960934409585, 66 | 13834722.5801401374, 124.688684161089762, 5228093.177931598, 67 | -0.56879356755666463, -0.56918731952397221, -9919582785894.853], 68 | [-87.85331, 85.66836, -65.120313040242748, 69 | 66.48646, 16.09921, -4.888658719272296, 70 | 17286615.3147144645, 155.58592449699137, 2635887.4729110181, 71 | -0.90697975771398578, -0.91095608883042767, 42667211366919.534], 72 | [1.74708, 128.32011, -101.584843631173858, 73 | -11.16617, 11.87109, -86.325793296437476, 74 | 12942901.1241347408, 116.650512484301857, 5682744.8413270572, 75 | -0.44857868222697644, -0.44824490340007729, 10763055294345.653], 76 | [-25.72959, -144.90758, -153.647468693117198, 77 | -57.70581, -269.17879, -48.343983158876487, 78 | 9413446.7452453107, 84.664533838404295, 6356176.6898881281, 79 | 0.09492245755254703, 0.09737058264766572, 74515122850712.444], 80 | [-41.22777, 122.32875, 14.285113402275739, 81 | -7.57291, 130.37946, 10.805303085187369, 82 | 3812686.035106021, 34.34330804743883, 3588703.8812128856, 83 | 0.82605222593217889, 0.82572158200920196, -2456961531057.857], 84 | [11.01307, 138.25278, 79.43682622782374, 85 | 6.62726, 247.05981, 103.708090215522657, 86 | 11911190.819018408, 107.341669954114577, 6070904.722786735, 87 | -0.29767608923657404, -0.29785143390252321, 17121631423099.696], 88 | [-29.47124, 95.14681, -163.779130441688382, 89 | -27.46601, -69.15955, -15.909335945554969, 90 | 13487015.8381145492, 121.294026715742277, 5481428.9945736388, 91 | -0.51527225545373252, -0.51556587964721788, 104679964020340.318]] 92 | 93 | def test_inverse(self): 94 | """Helper function for testing inverse calculation""" 95 | for l in GeodesicTest.testcases: 96 | (lat1, lon1, azi1, lat2, lon2, azi2, 97 | s12, a12, m12, M12, M21, S12) = l 98 | inv = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2, 99 | Geodesic.ALL | Geodesic.LONG_UNROLL) 100 | self.assertAlmostEqual(lon2, inv["lon2"], delta = 1e-13) 101 | self.assertAlmostEqual(azi1, inv["azi1"], delta = 1e-13) 102 | self.assertAlmostEqual(azi2, inv["azi2"], delta = 1e-13) 103 | self.assertAlmostEqual(s12, inv["s12"], delta = 1e-8) 104 | self.assertAlmostEqual(a12, inv["a12"], delta = 1e-13) 105 | self.assertAlmostEqual(m12, inv["m12"], delta = 1e-8) 106 | self.assertAlmostEqual(M12, inv["M12"], delta = 1e-15) 107 | self.assertAlmostEqual(M21, inv["M21"], delta = 1e-15) 108 | self.assertAlmostEqual(S12, inv["S12"], delta = 0.1) 109 | 110 | def test_direct(self): 111 | """Helper function for testing direct calculation""" 112 | for l in GeodesicTest.testcases: 113 | (lat1, lon1, azi1, lat2, lon2, azi2, 114 | s12, a12, m12, M12, M21, S12) = l 115 | direct = Geodesic.WGS84.Direct(lat1, lon1, azi1, s12, 116 | Geodesic.ALL | Geodesic.LONG_UNROLL) 117 | self.assertAlmostEqual(lat2, direct["lat2"], delta = 1e-13) 118 | self.assertAlmostEqual(lon2, direct["lon2"], delta = 1e-13) 119 | self.assertAlmostEqual(azi2, direct["azi2"], delta = 1e-13) 120 | self.assertAlmostEqual(a12, direct["a12"], delta = 1e-13) 121 | self.assertAlmostEqual(m12, direct["m12"], delta = 1e-8) 122 | self.assertAlmostEqual(M12, direct["M12"], delta = 1e-15) 123 | self.assertAlmostEqual(M21, direct["M21"], delta = 1e-15) 124 | self.assertAlmostEqual(S12, direct["S12"], delta = 0.1) 125 | 126 | def test_arcdirect(self): 127 | """Helper function for testing direct calculation with arc length""" 128 | for l in GeodesicTest.testcases: 129 | (lat1, lon1, azi1, lat2, lon2, azi2, 130 | s12, a12, m12, M12, M21, S12) = l 131 | direct = Geodesic.WGS84.ArcDirect(lat1, lon1, azi1, a12, 132 | Geodesic.ALL | Geodesic.LONG_UNROLL) 133 | self.assertAlmostEqual(lat2, direct["lat2"], delta = 1e-13) 134 | self.assertAlmostEqual(lon2, direct["lon2"], delta = 1e-13) 135 | self.assertAlmostEqual(azi2, direct["azi2"], delta = 1e-13) 136 | self.assertAlmostEqual(s12, direct["s12"], delta = 1e-8) 137 | self.assertAlmostEqual(m12, direct["m12"], delta = 1e-8) 138 | self.assertAlmostEqual(M12, direct["M12"], delta = 1e-15) 139 | self.assertAlmostEqual(M21, direct["M21"], delta = 1e-15) 140 | self.assertAlmostEqual(S12, direct["S12"], delta = 0.1) 141 | 142 | class GeodSolveTest(unittest.TestCase): 143 | """GeodSolve tests""" 144 | 145 | def test_GeodSolve0(self): 146 | """GeodSolve0""" 147 | inv = Geodesic.WGS84.Inverse(40.6, -73.8, 49.01666667, 2.55) 148 | self.assertAlmostEqual(inv["azi1"], 53.47022, delta = 0.5e-5) 149 | self.assertAlmostEqual(inv["azi2"], 111.59367, delta = 0.5e-5) 150 | self.assertAlmostEqual(inv["s12"], 5853226, delta = 0.5) 151 | 152 | def test_GeodSolve1(self): 153 | """GeodSolve1""" 154 | direct = Geodesic.WGS84.Direct(40.63972222, -73.77888889, 53.5, 5850e3) 155 | self.assertAlmostEqual(direct["lat2"], 49.01467, delta = 0.5e-5) 156 | self.assertAlmostEqual(direct["lon2"], 2.56106, delta = 0.5e-5) 157 | self.assertAlmostEqual(direct["azi2"], 111.62947, delta = 0.5e-5) 158 | 159 | def test_GeodSolve2(self): 160 | """Check fix for antipodal prolate bug found 2010-09-04""" 161 | geod = Geodesic(6.4e6, -1/150.0) 162 | inv = geod.Inverse(0.07476, 0, -0.07476, 180) 163 | self.assertAlmostEqual(inv["azi1"], 90.00078, delta = 0.5e-5) 164 | self.assertAlmostEqual(inv["azi2"], 90.00078, delta = 0.5e-5) 165 | self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) 166 | inv = geod.Inverse(0.1, 0, -0.1, 180) 167 | self.assertAlmostEqual(inv["azi1"], 90.00105, delta = 0.5e-5) 168 | self.assertAlmostEqual(inv["azi2"], 90.00105, delta = 0.5e-5) 169 | self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) 170 | 171 | def test_GeodSolve4(self): 172 | """Check fix for short line bug found 2010-05-21""" 173 | inv = Geodesic.WGS84.Inverse(36.493349428792, 0, 174 | 36.49334942879201, 0.0000008) 175 | self.assertAlmostEqual(inv["s12"], 0.072, delta = 0.5e-3) 176 | 177 | def test_GeodSolve5(self): 178 | """Check fix for point2=pole bug found 2010-05-03""" 179 | direct = Geodesic.WGS84.Direct(0.01777745589997, 30, 0, 10e6) 180 | self.assertAlmostEqual(direct["lat2"], 90, delta = 0.5e-5) 181 | if direct["lon2"] < 0: 182 | self.assertAlmostEqual(direct["lon2"], -150, delta = 0.5e-5) 183 | self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) 184 | else: 185 | self.assertAlmostEqual(direct["lon2"], 30, delta = 0.5e-5) 186 | self.assertAlmostEqual(direct["azi2"], 0, delta = 0.5e-5) 187 | 188 | def test_GeodSolve6(self): 189 | """Check fix for volatile sbet12a bug found 2011-06-25 (gcc 4.4.4 190 | x86 -O3). Found again on 2012-03-27 with tdm-mingw32 (g++ 4.6.1).""" 191 | inv = Geodesic.WGS84.Inverse(88.202499451857, 0, 192 | -88.202499451857, 179.981022032992859592) 193 | self.assertAlmostEqual(inv["s12"], 20003898.214, delta = 0.5e-3) 194 | inv = Geodesic.WGS84.Inverse(89.262080389218, 0, 195 | -89.262080389218, 179.992207982775375662) 196 | self.assertAlmostEqual(inv["s12"], 20003925.854, delta = 0.5e-3) 197 | inv = Geodesic.WGS84.Inverse(89.333123580033, 0, 198 | -89.333123580032997687, 179.99295812360148422) 199 | self.assertAlmostEqual(inv["s12"], 20003926.881, delta = 0.5e-3) 200 | 201 | def test_GeodSolve9(self): 202 | """Check fix for volatile x bug found 2011-06-25 (gcc 4.4.4 x86 -O3)""" 203 | inv = Geodesic.WGS84.Inverse(56.320923501171, 0, 204 | -56.320923501171, 179.664747671772880215) 205 | self.assertAlmostEqual(inv["s12"], 19993558.287, delta = 0.5e-3) 206 | 207 | def test_GeodSolve10(self): 208 | """Check fix for adjust tol1_ bug found 2011-06-25 (Visual Studio 209 | 10 rel + debug)""" 210 | inv = Geodesic.WGS84.Inverse(52.784459512564, 0, 211 | -52.784459512563990912, 179.634407464943777557) 212 | self.assertAlmostEqual(inv["s12"], 19991596.095, delta = 0.5e-3) 213 | 214 | def test_GeodSolve11(self): 215 | """Check fix for bet2 = -bet1 bug found 2011-06-25 (Visual Studio 216 | 10 rel + debug)""" 217 | inv = Geodesic.WGS84.Inverse(48.522876735459, 0, 218 | -48.52287673545898293, 179.599720456223079643) 219 | self.assertAlmostEqual(inv["s12"], 19989144.774, delta = 0.5e-3) 220 | 221 | def test_GeodSolve12(self): 222 | """Check fix for inverse geodesics on extreme prolate/oblate 223 | ellipsoids Reported 2012-08-29 Stefan Guenther 224 | ; fixed 2012-10-07""" 225 | geod = Geodesic(89.8, -1.83) 226 | inv = geod.Inverse(0, 0, -10, 160) 227 | self.assertAlmostEqual(inv["azi1"], 120.27, delta = 1e-2) 228 | self.assertAlmostEqual(inv["azi2"], 105.15, delta = 1e-2) 229 | self.assertAlmostEqual(inv["s12"], 266.7, delta = 1e-1) 230 | 231 | def test_GeodSolve14(self): 232 | """Check fix for inverse ignoring lon12 = nan""" 233 | inv = Geodesic.WGS84.Inverse(0, 0, 1, math.nan) 234 | self.assertTrue(math.isnan(inv["azi1"])) 235 | self.assertTrue(math.isnan(inv["azi2"])) 236 | self.assertTrue(math.isnan(inv["s12"])) 237 | 238 | def test_GeodSolve15(self): 239 | """Initial implementation of Math::eatanhe was wrong for e^2 < 0. This 240 | checks that this is fixed.""" 241 | geod = Geodesic(6.4e6, -1/150.0) 242 | direct = geod.Direct(1, 2, 3, 4, Geodesic.AREA) 243 | self.assertAlmostEqual(direct["S12"], 23700, delta = 0.5) 244 | 245 | def test_GeodSolve17(self): 246 | """Check fix for LONG_UNROLL bug found on 2015-05-07""" 247 | direct = Geodesic.WGS84.Direct(40, -75, -10, 2e7, 248 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 249 | self.assertAlmostEqual(direct["lat2"], -39, delta = 1) 250 | self.assertAlmostEqual(direct["lon2"], -254, delta = 1) 251 | self.assertAlmostEqual(direct["azi2"], -170, delta = 1) 252 | line = Geodesic.WGS84.Line(40, -75, -10) 253 | direct = line.Position(2e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) 254 | self.assertAlmostEqual(direct["lat2"], -39, delta = 1) 255 | self.assertAlmostEqual(direct["lon2"], -254, delta = 1) 256 | self.assertAlmostEqual(direct["azi2"], -170, delta = 1) 257 | direct = Geodesic.WGS84.Direct(40, -75, -10, 2e7) 258 | self.assertAlmostEqual(direct["lat2"], -39, delta = 1) 259 | self.assertAlmostEqual(direct["lon2"], 105, delta = 1) 260 | self.assertAlmostEqual(direct["azi2"], -170, delta = 1) 261 | direct = line.Position(2e7) 262 | self.assertAlmostEqual(direct["lat2"], -39, delta = 1) 263 | self.assertAlmostEqual(direct["lon2"], 105, delta = 1) 264 | self.assertAlmostEqual(direct["azi2"], -170, delta = 1) 265 | 266 | def test_GeodSolve26(self): 267 | """Check 0/0 problem with area calculation on sphere 2015-09-08""" 268 | geod = Geodesic(6.4e6, 0) 269 | inv = geod.Inverse(1, 2, 3, 4, Geodesic.AREA) 270 | self.assertAlmostEqual(inv["S12"], 49911046115.0, delta = 0.5) 271 | 272 | def test_GeodSolve28(self): 273 | """Check for bad placement of assignment of r.a12 with |f| > 0.01 (bug in 274 | Java implementation fixed on 2015-05-19).""" 275 | geod = Geodesic(6.4e6, 0.1) 276 | direct = geod.Direct(1, 2, 10, 5e6) 277 | self.assertAlmostEqual(direct["a12"], 48.55570690, delta = 0.5e-8) 278 | 279 | def test_GeodSolve29(self): 280 | """Check longitude unrolling with inverse calculation 2015-09-16""" 281 | direct = Geodesic.WGS84.Inverse(0, 539, 0, 181) 282 | self.assertAlmostEqual(direct["lon1"], 179, delta = 1e-10) 283 | self.assertAlmostEqual(direct["lon2"], -179, delta = 1e-10) 284 | self.assertAlmostEqual(direct["s12"], 222639, delta = 0.5) 285 | direct = Geodesic.WGS84.Inverse(0, 539, 0, 181, 286 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 287 | self.assertAlmostEqual(direct["lon1"], 539, delta = 1e-10) 288 | self.assertAlmostEqual(direct["lon2"], 541, delta = 1e-10) 289 | self.assertAlmostEqual(direct["s12"], 222639, delta = 0.5) 290 | 291 | def test_GeodSolve33(self): 292 | """Check max(-0.0,+0.0) issues 2015-08-22 (triggered by bugs in 293 | Octave -- sind(-0.0) = +0.0 -- and in some version of Visual 294 | Studio -- fmod(-0.0, 360.0) = +0.0.""" 295 | inv = Geodesic.WGS84.Inverse(0, 0, 0, 179) 296 | self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) 297 | self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) 298 | self.assertAlmostEqual(inv["s12"], 19926189, delta = 0.5) 299 | inv = Geodesic.WGS84.Inverse(0, 0, 0, 179.5) 300 | self.assertAlmostEqual(inv["azi1"], 55.96650, delta = 0.5e-5) 301 | self.assertAlmostEqual(inv["azi2"], 124.03350, delta = 0.5e-5) 302 | self.assertAlmostEqual(inv["s12"], 19980862, delta = 0.5) 303 | inv = Geodesic.WGS84.Inverse(0, 0, 0, 180) 304 | self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) 305 | self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) 306 | self.assertAlmostEqual(inv["s12"], 20003931, delta = 0.5) 307 | inv = Geodesic.WGS84.Inverse(0, 0, 1, 180) 308 | self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) 309 | self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) 310 | self.assertAlmostEqual(inv["s12"], 19893357, delta = 0.5) 311 | geod = Geodesic(6.4e6, 0) 312 | inv = geod.Inverse(0, 0, 0, 179) 313 | self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) 314 | self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) 315 | self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) 316 | inv = geod.Inverse(0, 0, 0, 180) 317 | self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) 318 | self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) 319 | self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) 320 | inv = geod.Inverse(0, 0, 1, 180) 321 | self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) 322 | self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) 323 | self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) 324 | geod = Geodesic(6.4e6, -1/300.0) 325 | inv = geod.Inverse(0, 0, 0, 179) 326 | self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) 327 | self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) 328 | self.assertAlmostEqual(inv["s12"], 19994492, delta = 0.5) 329 | inv = geod.Inverse(0, 0, 0, 180) 330 | self.assertAlmostEqual(inv["azi1"], 90.00000, delta = 0.5e-5) 331 | self.assertAlmostEqual(inv["azi2"], 90.00000, delta = 0.5e-5) 332 | self.assertAlmostEqual(inv["s12"], 20106193, delta = 0.5) 333 | inv = geod.Inverse(0, 0, 0.5, 180) 334 | self.assertAlmostEqual(inv["azi1"], 33.02493, delta = 0.5e-5) 335 | self.assertAlmostEqual(inv["azi2"], 146.97364, delta = 0.5e-5) 336 | self.assertAlmostEqual(inv["s12"], 20082617, delta = 0.5) 337 | inv = geod.Inverse(0, 0, 1, 180) 338 | self.assertAlmostEqual(inv["azi1"], 0.00000, delta = 0.5e-5) 339 | self.assertAlmostEqual(abs(inv["azi2"]), 180.00000, delta = 0.5e-5) 340 | self.assertAlmostEqual(inv["s12"], 20027270, delta = 0.5) 341 | 342 | def test_GeodSolve55(self): 343 | """Check fix for nan + point on equator or pole not returning all nans in 344 | Geodesic::Inverse, found 2015-09-23.""" 345 | inv = Geodesic.WGS84.Inverse(math.nan, 0, 0, 90) 346 | self.assertTrue(math.isnan(inv["azi1"])) 347 | self.assertTrue(math.isnan(inv["azi2"])) 348 | self.assertTrue(math.isnan(inv["s12"])) 349 | inv = Geodesic.WGS84.Inverse(math.nan, 0, 90, 9) 350 | self.assertTrue(math.isnan(inv["azi1"])) 351 | self.assertTrue(math.isnan(inv["azi2"])) 352 | self.assertTrue(math.isnan(inv["s12"])) 353 | 354 | def test_GeodSolve59(self): 355 | """Check for points close with longitudes close to 180 deg apart.""" 356 | inv = Geodesic.WGS84.Inverse(5, 0.00000000000001, 10, 180) 357 | self.assertAlmostEqual(inv["azi1"], 0.000000000000035, delta = 1.5e-14) 358 | self.assertAlmostEqual(inv["azi2"], 179.99999999999996, delta = 1.5e-14) 359 | self.assertAlmostEqual(inv["s12"], 18345191.174332713, delta = 5e-9) 360 | 361 | def test_GeodSolve61(self): 362 | """Make sure small negative azimuths are west-going""" 363 | direct = Geodesic.WGS84.Direct(45, 0, -0.000000000000000003, 1e7, 364 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 365 | self.assertAlmostEqual(direct["lat2"], 45.30632, delta = 0.5e-5) 366 | self.assertAlmostEqual(direct["lon2"], -180, delta = 0.5e-5) 367 | self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) 368 | line = Geodesic.WGS84.InverseLine(45, 0, 80, -0.000000000000000003) 369 | direct = line.Position(1e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) 370 | self.assertAlmostEqual(direct["lat2"], 45.30632, delta = 0.5e-5) 371 | self.assertAlmostEqual(direct["lon2"], -180, delta = 0.5e-5) 372 | self.assertAlmostEqual(abs(direct["azi2"]), 180, delta = 0.5e-5) 373 | 374 | def test_GeodSolve65(self): 375 | """Check for bug in east-going check in GeodesicLine (needed to check for 376 | sign of 0) and sign error in area calculation due to a bogus override 377 | of the code for alp12. Found/fixed on 2015-12-19.""" 378 | line = Geodesic.WGS84.InverseLine(30, -0.000000000000000001, -31, 180, 379 | Geodesic.ALL) 380 | direct = line.Position(1e7, Geodesic.ALL | Geodesic.LONG_UNROLL) 381 | self.assertAlmostEqual(direct["lat1"], 30.00000 , delta = 0.5e-5) 382 | self.assertAlmostEqual(direct["lon1"], -0.00000 , delta = 0.5e-5) 383 | self.assertAlmostEqual(abs(direct["azi1"]), 180.00000, delta = 0.5e-5) 384 | self.assertAlmostEqual(direct["lat2"], -60.23169 , delta = 0.5e-5) 385 | self.assertAlmostEqual(direct["lon2"], -0.00000 , delta = 0.5e-5) 386 | self.assertAlmostEqual(abs(direct["azi2"]), 180.00000, delta = 0.5e-5) 387 | self.assertAlmostEqual(direct["s12"] , 10000000 , delta = 0.5) 388 | self.assertAlmostEqual(direct["a12"] , 90.06544 , delta = 0.5e-5) 389 | self.assertAlmostEqual(direct["m12"] , 6363636 , delta = 0.5) 390 | self.assertAlmostEqual(direct["M12"] , -0.0012834, delta = 0.5e7) 391 | self.assertAlmostEqual(direct["M21"] , 0.0013749 , delta = 0.5e-7) 392 | self.assertAlmostEqual(direct["S12"] , 0 , delta = 0.5) 393 | direct = line.Position(2e7, Geodesic.ALL | Geodesic.LONG_UNROLL) 394 | self.assertAlmostEqual(direct["lat1"], 30.00000 , delta = 0.5e-5) 395 | self.assertAlmostEqual(direct["lon1"], -0.00000 , delta = 0.5e-5) 396 | self.assertAlmostEqual(abs(direct["azi1"]), 180.00000, delta = 0.5e-5) 397 | self.assertAlmostEqual(direct["lat2"], -30.03547 , delta = 0.5e-5) 398 | self.assertAlmostEqual(direct["lon2"], -180.00000, delta = 0.5e-5) 399 | self.assertAlmostEqual(direct["azi2"], -0.00000 , delta = 0.5e-5) 400 | self.assertAlmostEqual(direct["s12"] , 20000000 , delta = 0.5) 401 | self.assertAlmostEqual(direct["a12"] , 179.96459 , delta = 0.5e-5) 402 | self.assertAlmostEqual(direct["m12"] , 54342 , delta = 0.5) 403 | self.assertAlmostEqual(direct["M12"] , -1.0045592, delta = 0.5e7) 404 | self.assertAlmostEqual(direct["M21"] , -0.9954339, delta = 0.5e-7) 405 | self.assertAlmostEqual(direct["S12"] , 127516405431022.0, delta = 0.5) 406 | 407 | def test_GeodSolve66(self): 408 | """Check for InverseLine if line is slightly west of S and that s13 is 409 | correctly set.""" 410 | line = Geodesic.WGS84.InverseLine(-5, -0.000000000000002, -10, 180) 411 | direct = line.Position(2e7, Geodesic.STANDARD | Geodesic.LONG_UNROLL) 412 | self.assertAlmostEqual(direct["lat2"], 4.96445 , delta = 0.5e-5) 413 | self.assertAlmostEqual(direct["lon2"], -180.00000, delta = 0.5e-5) 414 | self.assertAlmostEqual(direct["azi2"], -0.00000 , delta = 0.5e-5) 415 | direct = line.Position(0.5 * line.s13, 416 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 417 | self.assertAlmostEqual(direct["lat2"], -87.52461 , delta = 0.5e-5) 418 | self.assertAlmostEqual(direct["lon2"], -0.00000 , delta = 0.5e-5) 419 | self.assertAlmostEqual(direct["azi2"], -180.00000, delta = 0.5e-5) 420 | 421 | def test_GeodSolve71(self): 422 | """Check that DirectLine sets s13.""" 423 | line = Geodesic.WGS84.DirectLine(1, 2, 45, 1e7) 424 | direct = line.Position(0.5 * line.s13, 425 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 426 | self.assertAlmostEqual(direct["lat2"], 30.92625, delta = 0.5e-5) 427 | self.assertAlmostEqual(direct["lon2"], 37.54640, delta = 0.5e-5) 428 | self.assertAlmostEqual(direct["azi2"], 55.43104, delta = 0.5e-5) 429 | 430 | def test_GeodSolve73(self): 431 | """Check for backwards from the pole bug reported by Anon on 2016-02-13. 432 | This only affected the Java implementation. It was introduced in Java 433 | version 1.44 and fixed in 1.46-SNAPSHOT on 2016-01-17. 434 | Also the + sign on azi2 is a check on the normalizing of azimuths 435 | (converting -0.0 to +0.0).""" 436 | direct = Geodesic.WGS84.Direct(90, 10, 180, -1e6) 437 | self.assertAlmostEqual(direct["lat2"], 81.04623, delta = 0.5e-5) 438 | self.assertAlmostEqual(direct["lon2"], -170, delta = 0.5e-5) 439 | self.assertAlmostEqual(direct["azi2"], 0, delta = 0.5e-5) 440 | self.assertTrue(math.copysign(1, direct["azi2"]) > 0) 441 | 442 | def test_GeodSolve74(self): 443 | """Check fix for inaccurate areas, bug introduced in v1.46, fixed 444 | 2015-10-16.""" 445 | inv = Geodesic.WGS84.Inverse(54.1589, 15.3872, 54.1591, 15.3877, 446 | Geodesic.ALL) 447 | self.assertAlmostEqual(inv["azi1"], 55.723110355, delta = 5e-9) 448 | self.assertAlmostEqual(inv["azi2"], 55.723515675, delta = 5e-9) 449 | self.assertAlmostEqual(inv["s12"], 39.527686385, delta = 5e-9) 450 | self.assertAlmostEqual(inv["a12"], 0.000355495, delta = 5e-9) 451 | self.assertAlmostEqual(inv["m12"], 39.527686385, delta = 5e-9) 452 | self.assertAlmostEqual(inv["M12"], 0.999999995, delta = 5e-9) 453 | self.assertAlmostEqual(inv["M21"], 0.999999995, delta = 5e-9) 454 | self.assertAlmostEqual(inv["S12"], 286698586.30197, delta = 5e-4) 455 | 456 | def test_GeodSolve76(self): 457 | """The distance from Wellington and Salamanca (a classic failure of 458 | Vincenty)""" 459 | inv = Geodesic.WGS84.Inverse(-(41+19/60.0), 174+49/60.0, 460 | 40+58/60.0, -(5+30/60.0)) 461 | self.assertAlmostEqual(inv["azi1"], 160.39137649664, delta = 0.5e-11) 462 | self.assertAlmostEqual(inv["azi2"], 19.50042925176, delta = 0.5e-11) 463 | self.assertAlmostEqual(inv["s12"], 19960543.857179, delta = 0.5e-6) 464 | 465 | def test_GeodSolve78(self): 466 | """An example where the NGS calculator fails to converge""" 467 | inv = Geodesic.WGS84.Inverse(27.2, 0.0, -27.1, 179.5) 468 | self.assertAlmostEqual(inv["azi1"], 45.82468716758, delta = 0.5e-11) 469 | self.assertAlmostEqual(inv["azi2"], 134.22776532670, delta = 0.5e-11) 470 | self.assertAlmostEqual(inv["s12"], 19974354.765767, delta = 0.5e-6) 471 | 472 | def test_GeodSolve80(self): 473 | """Some tests to add code coverage: computing scale in special cases + zero 474 | length geodesic (includes GeodSolve80 - GeodSolve83) + using an incapable 475 | line.""" 476 | inv = Geodesic.WGS84.Inverse(0, 0, 0, 90, Geodesic.GEODESICSCALE) 477 | self.assertAlmostEqual(inv["M12"], -0.00528427534, delta = 0.5e-10) 478 | self.assertAlmostEqual(inv["M21"], -0.00528427534, delta = 0.5e-10) 479 | 480 | inv = Geodesic.WGS84.Inverse(0, 0, 1e-6, 1e-6, Geodesic.GEODESICSCALE) 481 | self.assertAlmostEqual(inv["M12"], 1, delta = 0.5e-10) 482 | self.assertAlmostEqual(inv["M21"], 1, delta = 0.5e-10) 483 | 484 | inv = Geodesic.WGS84.Inverse(20.001, 0, 20.001, 0, Geodesic.ALL) 485 | self.assertAlmostEqual(inv["a12"], 0, delta = 1e-13) 486 | self.assertAlmostEqual(inv["s12"], 0, delta = 1e-8) 487 | self.assertAlmostEqual(inv["azi1"], 180, delta = 1e-13) 488 | self.assertAlmostEqual(inv["azi2"], 180, delta = 1e-13) 489 | self.assertAlmostEqual(inv["m12"], 0, delta = 1e-8) 490 | self.assertAlmostEqual(inv["M12"], 1, delta = 1e-15) 491 | self.assertAlmostEqual(inv["M21"], 1, delta = 1e-15) 492 | self.assertAlmostEqual(inv["S12"], 0, delta = 1e-10) 493 | self.assertTrue(math.copysign(1, inv["a12"]) > 0) 494 | self.assertTrue(math.copysign(1, inv["s12"]) > 0) 495 | self.assertTrue(math.copysign(1, inv["m12"]) > 0) 496 | 497 | inv = Geodesic.WGS84.Inverse(90, 0, 90, 180, Geodesic.ALL) 498 | self.assertAlmostEqual(inv["a12"], 0, delta = 1e-13) 499 | self.assertAlmostEqual(inv["s12"], 0, delta = 1e-8) 500 | self.assertAlmostEqual(inv["azi1"], 0, delta = 1e-13) 501 | self.assertAlmostEqual(inv["azi2"], 180, delta = 1e-13) 502 | self.assertAlmostEqual(inv["m12"], 0, delta = 1e-8) 503 | self.assertAlmostEqual(inv["M12"], 1, delta = 1e-15) 504 | self.assertAlmostEqual(inv["M21"], 1, delta = 1e-15) 505 | self.assertAlmostEqual(inv["S12"], 127516405431022.0, delta = 0.5) 506 | 507 | # An incapable line which can't take distance as input 508 | line = Geodesic.WGS84.Line(1, 2, 90, Geodesic.LATITUDE) 509 | direct = line.Position(1000, Geodesic.EMPTY) 510 | self.assertTrue(math.isnan(direct["a12"])) 511 | 512 | def test_GeodSolve84(self): 513 | """Tests for python implementation to check fix for range errors with 514 | {fmod,sin,cos}(inf) (includes GeodSolve84 - GeodSolve91).""" 515 | direct = Geodesic.WGS84.Direct(0, 0, 90, math.inf) 516 | self.assertTrue(math.isnan(direct["lat2"])) 517 | self.assertTrue(math.isnan(direct["lon2"])) 518 | self.assertTrue(math.isnan(direct["azi2"])) 519 | direct = Geodesic.WGS84.Direct(0, 0, 90, math.nan) 520 | self.assertTrue(math.isnan(direct["lat2"])) 521 | self.assertTrue(math.isnan(direct["lon2"])) 522 | self.assertTrue(math.isnan(direct["azi2"])) 523 | direct = Geodesic.WGS84.Direct(0, 0, math.inf, 1000) 524 | self.assertTrue(math.isnan(direct["lat2"])) 525 | self.assertTrue(math.isnan(direct["lon2"])) 526 | self.assertTrue(math.isnan(direct["azi2"])) 527 | direct = Geodesic.WGS84.Direct(0, 0, math.nan, 1000) 528 | self.assertTrue(math.isnan(direct["lat2"])) 529 | self.assertTrue(math.isnan(direct["lon2"])) 530 | self.assertTrue(math.isnan(direct["azi2"])) 531 | direct = Geodesic.WGS84.Direct(0, math.inf, 90, 1000) 532 | self.assertTrue(direct["lat1"] == 0) 533 | self.assertTrue(math.isnan(direct["lon2"])) 534 | self.assertTrue(direct["azi2"] == 90) 535 | direct = Geodesic.WGS84.Direct(0, math.nan, 90, 1000) 536 | self.assertTrue(direct["lat1"] == 0) 537 | self.assertTrue(math.isnan(direct["lon2"])) 538 | self.assertTrue(direct["azi2"] == 90) 539 | direct = Geodesic.WGS84.Direct(math.inf, 0, 90, 1000) 540 | self.assertTrue(math.isnan(direct["lat2"])) 541 | self.assertTrue(math.isnan(direct["lon2"])) 542 | self.assertTrue(math.isnan(direct["azi2"])) 543 | direct = Geodesic.WGS84.Direct(math.nan, 0, 90, 1000) 544 | self.assertTrue(math.isnan(direct["lat2"])) 545 | self.assertTrue(math.isnan(direct["lon2"])) 546 | self.assertTrue(math.isnan(direct["azi2"])) 547 | 548 | def test_GeodSolve92(self): 549 | """Check fix for inaccurate hypot with python 3.[89]. Problem reported 550 | by agdhruv https://github.com/geopy/geopy/issues/466 ; see 551 | https://bugs.python.org/issue43088""" 552 | inv = Geodesic.WGS84.Inverse(37.757540000000006, -122.47018, 553 | 37.75754, -122.470177) 554 | self.assertAlmostEqual(inv["azi1"], 89.99999923, delta = 1e-7 ) 555 | self.assertAlmostEqual(inv["azi2"], 90.00000106, delta = 1e-7 ) 556 | self.assertAlmostEqual(inv["s12"], 0.264, delta = 0.5e-3) 557 | 558 | def test_GeodSolve94(self): 559 | """Check fix for lat2 = nan being treated as lat2 = 0 (bug found 560 | 2021-07-26)""" 561 | inv = Geodesic.WGS84.Inverse(0, 0, math.nan, 90) 562 | self.assertTrue(math.isnan(inv["azi1"])) 563 | self.assertTrue(math.isnan(inv["azi2"])) 564 | self.assertTrue(math.isnan(inv["s12"])) 565 | 566 | def test_GeodSolve96(self): 567 | """Failure with long doubles found with test case from Nowak + Nowak Da 568 | Costa (2022). Problem was using somg12 > 1 as a test that it needed 569 | to be set when roundoff could result in somg12 slightly bigger that 1. 570 | Found + fixed 2022-03-30.""" 571 | geod = Geodesic(6378137, 1/298.257222101) 572 | inv = geod.Inverse(0, 0, 60.0832522871723, 89.8492185074635, Geodesic.AREA) 573 | self.assertAlmostEqual(inv["S12"], 42426932221845, delta = 0.5) 574 | 575 | class PlanimeterTest(unittest.TestCase): 576 | """Planimeter tests""" 577 | 578 | polygon = Geodesic.WGS84.Polygon(False) 579 | polyline = Geodesic.WGS84.Polygon(True) 580 | 581 | @staticmethod 582 | def Planimeter(points): 583 | """Helper function for polygons""" 584 | PlanimeterTest.polygon.Clear() 585 | for p in points: 586 | PlanimeterTest.polygon.AddPoint(p[0], p[1]) 587 | return PlanimeterTest.polygon.Compute(False, True) 588 | 589 | @staticmethod 590 | def PolyLength(points): 591 | """Helper function for polylines""" 592 | PlanimeterTest.polyline.Clear() 593 | for p in points: 594 | PlanimeterTest.polyline.AddPoint(p[0], p[1]) 595 | return PlanimeterTest.polyline.Compute(False, True) 596 | 597 | def test_Planimeter0(self): 598 | """Check fix for pole-encircling bug found 2011-03-16""" 599 | points = [[89, 0], [89, 90], [89, 180], [89, 270]] 600 | _, perimeter, area = PlanimeterTest.Planimeter(points) 601 | self.assertAlmostEqual(perimeter, 631819.8745, delta = 1e-4) 602 | self.assertAlmostEqual(area, 24952305678.0, delta = 1) 603 | points = [[-89, 0], [-89, 90], [-89, 180], [-89, 270]] 604 | _, perimeter, area = PlanimeterTest.Planimeter(points) 605 | self.assertAlmostEqual(perimeter, 631819.8745, delta = 1e-4) 606 | self.assertAlmostEqual(area, -24952305678.0, delta = 1) 607 | 608 | points = [[0, -1], [-1, 0], [0, 1], [1, 0]] 609 | _, perimeter, area = PlanimeterTest.Planimeter(points) 610 | self.assertAlmostEqual(perimeter, 627598.2731, delta = 1e-4) 611 | self.assertAlmostEqual(area, 24619419146.0, delta = 1) 612 | 613 | points = [[90, 0], [0, 0], [0, 90]] 614 | _, perimeter, area = PlanimeterTest.Planimeter(points) 615 | self.assertAlmostEqual(perimeter, 30022685, delta = 1) 616 | self.assertAlmostEqual(area, 63758202715511.0, delta = 1) 617 | _, perimeter, area = PlanimeterTest.PolyLength(points) 618 | self.assertAlmostEqual(perimeter, 20020719, delta = 1) 619 | self.assertTrue(math.isnan(area)) 620 | 621 | def test_Planimeter5(self): 622 | """Check fix for Planimeter pole crossing bug found 2011-06-24""" 623 | points = [[89, 0.1], [89, 90.1], [89, -179.9]] 624 | _, perimeter, area = PlanimeterTest.Planimeter(points) 625 | self.assertAlmostEqual(perimeter, 539297, delta = 1) 626 | self.assertAlmostEqual(area, 12476152838.5, delta = 1) 627 | 628 | def test_Planimeter6(self): 629 | """Check fix for Planimeter lon12 rounding bug found 2012-12-03""" 630 | points = [[9, -0.00000000000001], [9, 180], [9, 0]] 631 | _, perimeter, area = PlanimeterTest.Planimeter(points) 632 | self.assertAlmostEqual(perimeter, 36026861, delta = 1) 633 | self.assertAlmostEqual(area, 0, delta = 1) 634 | points = [[9, 0.00000000000001], [9, 0], [9, 180]] 635 | _, perimeter, area = PlanimeterTest.Planimeter(points) 636 | self.assertAlmostEqual(perimeter, 36026861, delta = 1) 637 | self.assertAlmostEqual(area, 0, delta = 1) 638 | points = [[9, 0.00000000000001], [9, 180], [9, 0]] 639 | _, perimeter, area = PlanimeterTest.Planimeter(points) 640 | self.assertAlmostEqual(perimeter, 36026861, delta = 1) 641 | self.assertAlmostEqual(area, 0, delta = 1) 642 | points = [[9, -0.00000000000001], [9, 0], [9, 180]] 643 | _, perimeter, area = PlanimeterTest.Planimeter(points) 644 | self.assertAlmostEqual(perimeter, 36026861, delta = 1) 645 | self.assertAlmostEqual(area, 0, delta = 1) 646 | 647 | def test_Planimeter12(self): 648 | """Area of arctic circle (not really -- adjunct to rhumb-area test)""" 649 | points = [[66.562222222, 0], [66.562222222, 180], [66.562222222, 360]] 650 | _, perimeter, area = PlanimeterTest.Planimeter(points) 651 | self.assertAlmostEqual(perimeter, 10465729, delta = 1) 652 | self.assertAlmostEqual(area, 0, delta = 1) 653 | 654 | def test_Planimeter12r(self): 655 | """Reverse area of arctic circle""" 656 | points = [[66.562222222, -0], [66.562222222, -180], [66.562222222, -360]] 657 | _, perimeter, area = PlanimeterTest.Planimeter(points) 658 | self.assertAlmostEqual(perimeter, 10465729, delta = 1) 659 | self.assertAlmostEqual(area, 0, delta = 1) 660 | 661 | def test_Planimeter13(self): 662 | """Check encircling pole twice""" 663 | points = [[89,-360], [89,-240], [89,-120], [89,0], [89,120], [89,240]] 664 | _, perimeter, area = PlanimeterTest.Planimeter(points) 665 | self.assertAlmostEqual(perimeter, 1160741, delta = 1) 666 | self.assertAlmostEqual(area, 32415230256.0, delta = 1) 667 | 668 | def test_Planimeter15(self): 669 | """Coverage tests, includes Planimeter15 - Planimeter18 (combinations of 670 | reverse and sign) + calls to testpoint, testedge.""" 671 | lat = [2, 1, 3] 672 | lon = [1, 2, 3] 673 | r = 18454562325.45119 674 | a0 = 510065621724088.5093 # ellipsoid area 675 | PlanimeterTest.polygon.Clear() 676 | PlanimeterTest.polygon.AddPoint(lat[0], lon[0]) 677 | PlanimeterTest.polygon.AddPoint(lat[1], lon[1]) 678 | _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], False, True) 679 | self.assertAlmostEqual(area, r, delta = 0.5) 680 | _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], False, False) 681 | self.assertAlmostEqual(area, r, delta = 0.5) 682 | _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], True, True) 683 | self.assertAlmostEqual(area, -r, delta = 0.5) 684 | _, _, area = PlanimeterTest.polygon.TestPoint(lat[2], lon[2], True, False) 685 | self.assertAlmostEqual(area, a0-r, delta = 0.5) 686 | inv = Geodesic.WGS84.Inverse(lat[1], lon[1], lat[2], lon[2]) 687 | azi1 = inv["azi1"] 688 | s12 = inv["s12"] 689 | _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, False, True) 690 | self.assertAlmostEqual(area, r, delta = 0.5) 691 | _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, False, False) 692 | self.assertAlmostEqual(area, r, delta = 0.5) 693 | _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, True, True) 694 | self.assertAlmostEqual(area, -r, delta = 0.5) 695 | _, _, area = PlanimeterTest.polygon.TestEdge(azi1, s12, True, False) 696 | self.assertAlmostEqual(area, a0-r, delta = 0.5) 697 | PlanimeterTest.polygon.AddPoint(lat[2], lon[2]) 698 | _, _, area = PlanimeterTest.polygon.Compute(False, True) 699 | self.assertAlmostEqual(area, r, delta = 0.5) 700 | _, _, area = PlanimeterTest.polygon.Compute(False, False) 701 | self.assertAlmostEqual(area, r, delta = 0.5) 702 | _, _, area = PlanimeterTest.polygon.Compute(True, True) 703 | self.assertAlmostEqual(area, -r, delta = 0.5) 704 | _, _, area = PlanimeterTest.polygon.Compute(True, False) 705 | self.assertAlmostEqual(area, a0-r, delta = 0.5) 706 | 707 | def test_Planimeter19(self): 708 | """Coverage tests, includes Planimeter19 - Planimeter20 (degenerate 709 | polygons) + extra cases.""" 710 | PlanimeterTest.polygon.Clear() 711 | _, perimeter, area = PlanimeterTest.polygon.Compute(False, True) 712 | self.assertTrue(area == 0) 713 | self.assertTrue(perimeter == 0) 714 | _, perimeter, area = PlanimeterTest.polygon.TestPoint(1, 1, False, True) 715 | self.assertTrue(area == 0) 716 | self.assertTrue(perimeter == 0) 717 | _, perimeter, area = PlanimeterTest.polygon.TestEdge(90, 1000, False, True) 718 | self.assertTrue(math.isnan(area)) 719 | self.assertTrue(math.isnan(perimeter)) 720 | PlanimeterTest.polygon.AddPoint(1, 1) 721 | _, perimeter, area = PlanimeterTest.polygon.Compute(False, True) 722 | self.assertTrue(area == 0) 723 | self.assertTrue(perimeter == 0) 724 | PlanimeterTest.polyline.Clear() 725 | _, perimeter, area = PlanimeterTest.polyline.Compute(False, True) 726 | self.assertTrue(perimeter == 0) 727 | _, perimeter, area = PlanimeterTest.polyline.TestPoint(1, 1, False, True) 728 | self.assertTrue(perimeter == 0) 729 | _, perimeter, area = PlanimeterTest.polyline.TestEdge(90, 1000, False, True) 730 | self.assertTrue(math.isnan(perimeter)) 731 | PlanimeterTest.polyline.AddPoint(1, 1) 732 | _, perimeter, area = PlanimeterTest.polyline.Compute(False, True) 733 | self.assertTrue(perimeter == 0) 734 | PlanimeterTest.polygon.AddPoint(1, 1) 735 | _, perimeter, area = PlanimeterTest.polyline.TestEdge(90, 1000, False, True) 736 | self.assertAlmostEqual(perimeter, 1000, delta = 1e-10) 737 | _, perimeter, area = PlanimeterTest.polyline.TestPoint(2, 2, False, True) 738 | self.assertAlmostEqual(perimeter, 156876.149, delta = 0.5e-3) 739 | 740 | def test_Planimeter21(self): 741 | """Some tests to add code coverage: multiple circlings of pole (includes 742 | Planimeter21 - Planimeter28) + invocations via testpoint and testedge.""" 743 | lat = 45 744 | azi = 39.2144607176828184218 745 | s = 8420705.40957178156285 746 | r = 39433884866571.4277 # Area for one circuit 747 | a0 = 510065621724088.5093 # Ellipsoid area 748 | PlanimeterTest.polygon.Clear() 749 | PlanimeterTest.polygon.AddPoint(lat, 60) 750 | PlanimeterTest.polygon.AddPoint(lat, 180) 751 | PlanimeterTest.polygon.AddPoint(lat, -60) 752 | PlanimeterTest.polygon.AddPoint(lat, 60) 753 | PlanimeterTest.polygon.AddPoint(lat, 180) 754 | PlanimeterTest.polygon.AddPoint(lat, -60) 755 | for i in [3, 4]: 756 | PlanimeterTest.polygon.AddPoint(lat, 60) 757 | PlanimeterTest.polygon.AddPoint(lat, 180) 758 | _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, False, True) 759 | self.assertAlmostEqual(area, i*r, delta = 0.5) 760 | _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, False, False) 761 | self.assertAlmostEqual(area, i*r, delta = 0.5) 762 | _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, True, True) 763 | self.assertAlmostEqual(area, -i*r, delta = 0.5) 764 | _, _, area = PlanimeterTest.polygon.TestPoint(lat, -60, True, False) 765 | self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) 766 | _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, False, True) 767 | self.assertAlmostEqual(area, i*r, delta = 0.5) 768 | _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, False, False) 769 | self.assertAlmostEqual(area, i*r, delta = 0.5) 770 | _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, True, True) 771 | self.assertAlmostEqual(area, -i*r, delta = 0.5) 772 | _, _, area = PlanimeterTest.polygon.TestEdge(azi, s, True, False) 773 | self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) 774 | PlanimeterTest.polygon.AddPoint(lat, -60) 775 | _, _, area = PlanimeterTest.polygon.Compute(False, True) 776 | self.assertAlmostEqual(area, i*r, delta = 0.5) 777 | _, _, area = PlanimeterTest.polygon.Compute(False, False) 778 | self.assertAlmostEqual(area, i*r, delta = 0.5) 779 | _, _, area = PlanimeterTest.polygon.Compute(True, True) 780 | self.assertAlmostEqual(area, -i*r, delta = 0.5) 781 | _, _, area = PlanimeterTest.polygon.Compute(True, False) 782 | self.assertAlmostEqual(area, -i*r + a0, delta = 0.5) 783 | 784 | def test_Planimeter29(self): 785 | """Check fix to transitdirect vs transit zero handling inconsistency""" 786 | PlanimeterTest.polygon.Clear() 787 | PlanimeterTest.polygon.AddPoint(0, 0) 788 | PlanimeterTest.polygon.AddEdge( 90, 1000) 789 | PlanimeterTest.polygon.AddEdge( 0, 1000) 790 | PlanimeterTest.polygon.AddEdge(-90, 1000) 791 | _, _, area = PlanimeterTest.polygon.Compute(False, True) 792 | # The area should be 1e6. Prior to the fix it was 1e6 - A/2, where 793 | # A = ellipsoid area. 794 | self.assertAlmostEqual(area, 1000000.0, delta = 0.01) 795 | -------------------------------------------------------------------------------- /geographiclib/test/test_sign.py: -------------------------------------------------------------------------------- 1 | """Geodesic tests""" 2 | 3 | import unittest 4 | import math 5 | import sys 6 | 7 | from geographiclib.geomath import Math 8 | from geographiclib.geodesic import Geodesic 9 | 10 | class SignTest(unittest.TestCase): 11 | """Sign test suite""" 12 | 13 | @staticmethod 14 | def equiv(x, y): 15 | """Test for equivalence""" 16 | 17 | return ( (math.isnan(x) and math.isnan(y)) or 18 | (x == y and math.copysign(1.0, x) == math.copysign(1.0, y)) ) 19 | 20 | def test_AngRound(self): 21 | """Test special cases for AngRound""" 22 | eps = sys.float_info.epsilon 23 | self.assertTrue(SignTest.equiv(Math.AngRound(-eps/32), -eps/32)) 24 | self.assertTrue(SignTest.equiv(Math.AngRound(-eps/64), -0.0 )) 25 | self.assertTrue(SignTest.equiv(Math.AngRound(- 0.0 ), -0.0 )) 26 | self.assertTrue(SignTest.equiv(Math.AngRound( 0.0 ), +0.0 )) 27 | self.assertTrue(SignTest.equiv(Math.AngRound( eps/64), +0.0 )) 28 | self.assertTrue(SignTest.equiv(Math.AngRound( eps/32), +eps/32)) 29 | self.assertTrue(SignTest.equiv(Math.AngRound((1-2*eps)/64), (1-2*eps)/64)) 30 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/64), 1.0 /64)) 31 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/64), 1.0 /64)) 32 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/64), 1.0 /64)) 33 | self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /64), 1.0 /64)) 34 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/64), 1.0 /64)) 35 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/64), 1.0 /64)) 36 | self.assertTrue(SignTest.equiv(Math.AngRound((1+2*eps)/64), (1+2*eps)/64)) 37 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/32), (1-eps )/32)) 38 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/32), 1.0 /32)) 39 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/32), 1.0 /32)) 40 | self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /32), 1.0 /32)) 41 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/32), 1.0 /32)) 42 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/32), (1+eps )/32)) 43 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/16), (1-eps )/16)) 44 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/16), (1-eps/2)/16)) 45 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/16), 1.0 /16)) 46 | self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 /16), 1.0 /16)) 47 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/4)/16), 1.0 /16)) 48 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/16), 1.0 /16)) 49 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/16), (1+eps )/16)) 50 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps )/ 8), (1-eps )/ 8)) 51 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/2)/ 8), (1-eps/2)/ 8)) 52 | self.assertTrue(SignTest.equiv(Math.AngRound((1-eps/4)/ 8), 1.0 / 8)) 53 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps/2)/ 8), 1.0 / 8)) 54 | self.assertTrue(SignTest.equiv(Math.AngRound((1+eps )/ 8), (1+eps )/ 8)) 55 | self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps ), 1-eps )) 56 | self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps/2 ), 1-eps/2 )) 57 | self.assertTrue(SignTest.equiv(Math.AngRound( 1-eps/4 ), 1 )) 58 | self.assertTrue(SignTest.equiv(Math.AngRound( 1.0 ), 1 )) 59 | self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps/4 ), 1 )) 60 | self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps/2 ), 1 )) 61 | self.assertTrue(SignTest.equiv(Math.AngRound( 1+eps ), 1+ eps )) 62 | self.assertTrue(SignTest.equiv(Math.AngRound( 90.0-64*eps), 90-64*eps )) 63 | self.assertTrue(SignTest.equiv(Math.AngRound( 90.0-32*eps), 90 )) 64 | self.assertTrue(SignTest.equiv(Math.AngRound( 90.0 ), 90 )) 65 | 66 | def test_sincosd(self): 67 | """Test special cases for sincosd""" 68 | inf = math.inf 69 | nan = math.nan 70 | s, c = Math.sincosd(- inf) 71 | self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) 72 | s, c = Math.sincosd(-810.0) 73 | self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) 74 | s, c = Math.sincosd(-720.0) 75 | self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) 76 | s, c = Math.sincosd(-630.0) 77 | self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) 78 | s, c = Math.sincosd(-540.0) 79 | self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, -1.0)) 80 | s, c = Math.sincosd(-450.0) 81 | self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) 82 | s, c = Math.sincosd(-360.0) 83 | self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) 84 | s, c = Math.sincosd(-270.0) 85 | self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) 86 | s, c = Math.sincosd(-180.0) 87 | self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, -1.0)) 88 | s, c = Math.sincosd(- 90.0) 89 | self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) 90 | s, c = Math.sincosd(- 0.0) 91 | self.assertTrue(SignTest.equiv(s, -0.0) and SignTest.equiv(c, +1.0)) 92 | s, c = Math.sincosd(+ 0.0) 93 | self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) 94 | s, c = Math.sincosd(+ 90.0) 95 | self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) 96 | s, c = Math.sincosd(+180.0) 97 | self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, -1.0)) 98 | s, c = Math.sincosd(+270.0) 99 | self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) 100 | s, c = Math.sincosd(+360.0) 101 | self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) 102 | s, c = Math.sincosd(+450.0) 103 | self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) 104 | s, c = Math.sincosd(+540.0) 105 | self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, -1.0)) 106 | s, c = Math.sincosd(+630.0) 107 | self.assertTrue(SignTest.equiv(s, -1.0) and SignTest.equiv(c, +0.0)) 108 | s, c = Math.sincosd(+720.0) 109 | self.assertTrue(SignTest.equiv(s, +0.0) and SignTest.equiv(c, +1.0)) 110 | s, c = Math.sincosd(+810.0) 111 | self.assertTrue(SignTest.equiv(s, +1.0) and SignTest.equiv(c, +0.0)) 112 | s, c = Math.sincosd(+ inf) 113 | self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) 114 | s, c = Math.sincosd( nan) 115 | self.assertTrue(SignTest.equiv(s, nan) and SignTest.equiv(c, nan)) 116 | 117 | def test_sincosd2(self): 118 | """Test accuracy of sincosd""" 119 | s1, c1 = Math.sincosd( 9.0) 120 | s2, c2 = Math.sincosd( 81.0) 121 | s3, c3 = Math.sincosd(-123456789.0) 122 | self.assertTrue(SignTest.equiv(s1, c2)) 123 | self.assertTrue(SignTest.equiv(s1, s3)) 124 | self.assertTrue(SignTest.equiv(c1, s2)) 125 | self.assertTrue(SignTest.equiv(c1,-c3)) 126 | 127 | def test_atan2d(self): 128 | """Test special cases for atan2d""" 129 | inf = math.inf 130 | nan = math.nan 131 | self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , -0.0 ), +180)) 132 | self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , -0.0 ), -180)) 133 | self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , +0.0 ), +0.0)) 134 | self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , +0.0 ), -0.0)) 135 | self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , -1.0 ), +180)) 136 | self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , -1.0 ), -180)) 137 | self.assertTrue(SignTest.equiv(Math.atan2d(+0.0 , +1.0 ), +0.0)) 138 | self.assertTrue(SignTest.equiv(Math.atan2d(-0.0 , +1.0 ), -0.0)) 139 | self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , +0.0 ), -90)) 140 | self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , -0.0 ), -90)) 141 | self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , +0.0 ), +90)) 142 | self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , -0.0 ), +90)) 143 | self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , -inf), +180)) 144 | self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , -inf), -180)) 145 | self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , +inf), +0.0)) 146 | self.assertTrue(SignTest.equiv(Math.atan2d(-1.0 , +inf), -0.0)) 147 | self.assertTrue(SignTest.equiv(Math.atan2d( +inf, +1.0 ), +90)) 148 | self.assertTrue(SignTest.equiv(Math.atan2d( +inf, -1.0 ), +90)) 149 | self.assertTrue(SignTest.equiv(Math.atan2d( -inf, +1.0 ), -90)) 150 | self.assertTrue(SignTest.equiv(Math.atan2d( -inf, -1.0 ), -90)) 151 | self.assertTrue(SignTest.equiv(Math.atan2d( +inf, -inf), +135)) 152 | self.assertTrue(SignTest.equiv(Math.atan2d( -inf, -inf), -135)) 153 | self.assertTrue(SignTest.equiv(Math.atan2d( +inf, +inf), +45)) 154 | self.assertTrue(SignTest.equiv(Math.atan2d( -inf, +inf), -45)) 155 | self.assertTrue(SignTest.equiv(Math.atan2d( nan, +1.0 ), nan)) 156 | self.assertTrue(SignTest.equiv(Math.atan2d(+1.0 , nan), nan)) 157 | 158 | def test_atan2d2(self): 159 | """Test accuracy of atan2d""" 160 | s = 7e-16 161 | self.assertEqual(Math.atan2d(s, -1.0), 180 - Math.atan2d(s, 1.0)) 162 | 163 | def test_sum(self): 164 | """Test special cases of sum""" 165 | s,_ = Math.sum(+9.0, -9.0); self.assertTrue(SignTest.equiv(s, +0.0)) 166 | s,_ = Math.sum(-9.0, +9.0); self.assertTrue(SignTest.equiv(s, +0.0)) 167 | s,_ = Math.sum(-0.0, +0.0); self.assertTrue(SignTest.equiv(s, +0.0)) 168 | s,_ = Math.sum(+0.0, -0.0); self.assertTrue(SignTest.equiv(s, +0.0)) 169 | s,_ = Math.sum(-0.0, -0.0); self.assertTrue(SignTest.equiv(s, -0.0)) 170 | s,_ = Math.sum(+0.0, +0.0); self.assertTrue(SignTest.equiv(s, +0.0)) 171 | 172 | def test_AngNormalize(self): 173 | """Test special cases of AngNormalize""" 174 | self.assertTrue(SignTest.equiv(Math.AngNormalize(-900.0), -180)) 175 | self.assertTrue(SignTest.equiv(Math.AngNormalize(-720.0), -0.0)) 176 | self.assertTrue(SignTest.equiv(Math.AngNormalize(-540.0), -180)) 177 | self.assertTrue(SignTest.equiv(Math.AngNormalize(-360.0), -0.0)) 178 | self.assertTrue(SignTest.equiv(Math.AngNormalize(-180.0), -180)) 179 | self.assertTrue(SignTest.equiv(Math.AngNormalize( -0.0), -0.0)) 180 | self.assertTrue(SignTest.equiv(Math.AngNormalize( +0.0), +0.0)) 181 | self.assertTrue(SignTest.equiv(Math.AngNormalize( 180.0), +180)) 182 | self.assertTrue(SignTest.equiv(Math.AngNormalize( 360.0), +0.0)) 183 | self.assertTrue(SignTest.equiv(Math.AngNormalize( 540.0), +180)) 184 | self.assertTrue(SignTest.equiv(Math.AngNormalize( 720.0), +0.0)) 185 | self.assertTrue(SignTest.equiv(Math.AngNormalize( 900.0), +180)) 186 | 187 | def test_AngDiff(self): 188 | """Test special cases of AngDiff""" 189 | eps = sys.float_info.epsilon 190 | s,_ = Math.AngDiff(+ 0.0,+ 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) 191 | s,_ = Math.AngDiff(+ 0.0,- 0.0); self.assertTrue(SignTest.equiv(s,-0.0 )) 192 | s,_ = Math.AngDiff(- 0.0,+ 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) 193 | s,_ = Math.AngDiff(- 0.0,- 0.0); self.assertTrue(SignTest.equiv(s,+0.0 )) 194 | s,_ = Math.AngDiff(+ 5.0,+365.0); self.assertTrue(SignTest.equiv(s,+0.0 )) 195 | s,_ = Math.AngDiff(+365.0,+ 5.0); self.assertTrue(SignTest.equiv(s,-0.0 )) 196 | s,_ = Math.AngDiff(+ 5.0,+185.0); self.assertTrue(SignTest.equiv(s,+180.0)) 197 | s,_ = Math.AngDiff(+185.0,+ 5.0); self.assertTrue(SignTest.equiv(s,-180.0)) 198 | s,_ = Math.AngDiff( +eps ,+180.0); self.assertTrue(SignTest.equiv(s,+180.0)) 199 | s,_ = Math.AngDiff( -eps ,+180.0); self.assertTrue(SignTest.equiv(s,-180.0)) 200 | s,_ = Math.AngDiff( +eps ,-180.0); self.assertTrue(SignTest.equiv(s,+180.0)) 201 | s,_ = Math.AngDiff( -eps ,-180.0); self.assertTrue(SignTest.equiv(s,-180.0)) 202 | 203 | def test_AngDiff2(self): 204 | """Test accuracy of AngDiff""" 205 | eps = sys.float_info.epsilon 206 | x = 138 + 128 * eps; y = -164; s,_ = Math.AngDiff(x, y) 207 | self.assertEqual(s, 58 - 128 * eps) 208 | 209 | def test_equatorial_coincident(self): 210 | """ 211 | azimuth with coincident point on equator 212 | """ 213 | # lat1 lat2 azi1/2 214 | C = [ 215 | [ +0.0, -0.0, 180 ], 216 | [ -0.0, +0.0, 0 ] 217 | ] 218 | for l in C: 219 | (lat1, lat2, azi) = l 220 | inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, 0.0) 221 | self.assertTrue(SignTest.equiv(inv["azi1"], azi)) 222 | self.assertTrue(SignTest.equiv(inv["azi2"], azi)) 223 | 224 | def test_equatorial_NS(self): 225 | """Does the nearly antipodal equatorial solution go north or south?""" 226 | # lat1 lat2 azi1 azi2 227 | C = [ 228 | [ +0.0, +0.0, 56, 124], 229 | [ -0.0, -0.0, 124, 56] 230 | ] 231 | for l in C: 232 | (lat1, lat2, azi1, azi2) = l 233 | inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, 179.5) 234 | self.assertAlmostEqual(inv["azi1"], azi1, delta = 1) 235 | self.assertAlmostEqual(inv["azi2"], azi2, delta = 1) 236 | 237 | def test_antipodal(self): 238 | """How does the exact antipodal equatorial path go N/S + E/W""" 239 | # lat1 lat2 lon2 azi1 azi2 240 | C = [ 241 | [ +0.0, +0.0, +180, +0.0, +180], 242 | [ -0.0, -0.0, +180, +180, +0.0], 243 | [ +0.0, +0.0, -180, -0.0, -180], 244 | [ -0.0, -0.0, -180, -180, -0.0] 245 | ] 246 | for l in C: 247 | (lat1, lat2, lon2, azi1, azi2) = l 248 | inv = Geodesic.WGS84.Inverse(lat1, 0.0, lat2, lon2) 249 | self.assertTrue(SignTest.equiv(inv["azi1"], azi1)) 250 | self.assertTrue(SignTest.equiv(inv["azi2"], azi2)) 251 | 252 | def test_antipodal_prolate(self): 253 | """Antipodal points on the equator with prolate ellipsoid""" 254 | # lon2 azi1/2 255 | C = [ 256 | [ +180, +90 ], 257 | [ -180, -90 ] 258 | ] 259 | geod = Geodesic(6.4e6, -1/300.0) 260 | for l in C: 261 | (lon2, azi) = l 262 | inv = geod.Inverse(0.0, 0.0, 0.0, lon2) 263 | self.assertTrue(SignTest.equiv(inv["azi1"], azi)) 264 | self.assertTrue(SignTest.equiv(inv["azi2"], azi)) 265 | 266 | def test_azimuth_0_180(self): 267 | """azimuths = +/-0 and +/-180 for the direct problem""" 268 | # azi1, lon2, azi2 269 | C = [ 270 | [ +0.0, +180, +180 ], 271 | [ -0.0, -180, -180 ], 272 | [ +180, +180, +0.0 ], 273 | [ -180, -180, -0.0 ] 274 | ] 275 | for l in C: 276 | (azi1, lon2, azi2) = l 277 | direct = Geodesic.WGS84.Direct(0.0, 0.0, azi1, 15e6, 278 | Geodesic.STANDARD | Geodesic.LONG_UNROLL) 279 | self.assertTrue(SignTest.equiv(direct["lon2"], lon2)) 280 | self.assertTrue(SignTest.equiv(direct["azi2"], azi2)) 281 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.cfg.in: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = geographiclib 3 | version = attr: geographiclib.__version__ 4 | author = Charles Karney 5 | author_email = charles@karney.com 6 | description = The geodesic routines from GeographicLib 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | keywords = gis, geographical, earth, distance, geodesic 10 | license = MIT 11 | license_files = LICENSE 12 | url = https://geographiclib.sourceforge.io/Python/ 13 | project_urls = 14 | Source Code = https://github.com/geographiclib/geographiclib-python 15 | Documentation = https://geographiclib.sourceforge.io/Python/doc/ 16 | Download = https://pypi.org/project/geographiclib/ 17 | classifiers = 18 | Development Status :: 5 - Production/Stable 19 | Intended Audience :: Developers 20 | Intended Audience :: Science/Research 21 | License :: OSI Approved :: MIT License 22 | Operating System :: OS Independent 23 | Programming Language :: Python 24 | Topic :: Scientific/Engineering :: GIS 25 | Topic :: Software Development :: Libraries :: Python Modules 26 | 27 | [options] 28 | packages = find: 29 | python_requires = >= @Python_VERSION_NUMBER@ 30 | --------------------------------------------------------------------------------