diff --git a/.vscode/settings.json b/.vscode/settings.json index 74c1583..158dbcf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "editor.formatOnSave": true, "modulename": "${workspaceFolderBasename}", "distname": "${workspaceFolderBasename}", - "moduleversion": "1.0.39", + "moduleversion": "1.0.40", } \ No newline at end of file diff --git a/README.md b/README.md index 9d5e37c..b065523 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ b'$EIGNQ,RMC*24\r\n' - `haversine` - finds great circle distance in km between two sets of (lat, lon) coordinates - `planar` - finds planar distance in m between two sets of (lat, lon) coordinates - `bearing` - finds bearing in degrees between two sets of (lat, lon) coordinates + - `area` - finds spherical area bounded by two sets of (lat, lon) coordinates See [Sphinx documentation](https://www.semuconsulting.com/pynmeagps/pynmeagps.html#module-pynmeagaps.nmeahelpers) for details. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 92f26e2..2c128f2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,13 @@ # pynmeagps Release Notes +### RELEASE 1.0.40 + +CHANGES: + +1. Add area() helper method to calculate spherical area of bounding box. +1. Sphinx documentation and docstrings enhanced to include global constants and decodes. +1. `socket_stream.SocketStream` class renamed to `socket_wrapper.SocketWrapper` class for clarity. + ### RELEASE 1.0.39 ENHANCEMENTS: diff --git a/docs/pynmeagps.rst b/docs/pynmeagps.rst index ff6d42d..803322a 100644 --- a/docs/pynmeagps.rst +++ b/docs/pynmeagps.rst @@ -1,85 +1,85 @@ -pynmeagps package -================= - -Submodules ----------- - -pynmeagps.exceptions module ---------------------------- - -.. automodule:: pynmeagps.exceptions - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeahelpers module ----------------------------- - -.. automodule:: pynmeagps.nmeahelpers - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeamessage module ----------------------------- - -.. automodule:: pynmeagps.nmeamessage - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeareader module ---------------------------- - -.. automodule:: pynmeagps.nmeareader - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeatypes\_core module --------------------------------- - -.. automodule:: pynmeagps.nmeatypes_core - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeatypes\_get module -------------------------------- - -.. automodule:: pynmeagps.nmeatypes_get - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeatypes\_poll module --------------------------------- - -.. automodule:: pynmeagps.nmeatypes_poll - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.nmeatypes\_set module -------------------------------- - -.. automodule:: pynmeagps.nmeatypes_set - :members: - :undoc-members: - :show-inheritance: - -pynmeagps.socket\_stream module -------------------------------- - -.. automodule:: pynmeagps.socket_stream - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: pynmeagps - :members: - :undoc-members: - :show-inheritance: +pynmeagps package +================= + +Submodules +---------- + +pynmeagps.exceptions module +--------------------------- + +.. automodule:: pynmeagps.exceptions + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeahelpers module +---------------------------- + +.. automodule:: pynmeagps.nmeahelpers + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeamessage module +---------------------------- + +.. automodule:: pynmeagps.nmeamessage + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeareader module +--------------------------- + +.. automodule:: pynmeagps.nmeareader + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeatypes\_core module +-------------------------------- + +.. automodule:: pynmeagps.nmeatypes_core + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeatypes\_get module +------------------------------- + +.. automodule:: pynmeagps.nmeatypes_get + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeatypes\_poll module +-------------------------------- + +.. automodule:: pynmeagps.nmeatypes_poll + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.nmeatypes\_set module +------------------------------- + +.. automodule:: pynmeagps.nmeatypes_set + :members: + :undoc-members: + :show-inheritance: + +pynmeagps.socket\_wrapper module +-------------------------------- + +.. automodule:: pynmeagps.socket_wrapper + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: pynmeagps + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/utilities.py b/examples/utilities.py index 203c65b..42bcab3 100644 --- a/examples/utilities.py +++ b/examples/utilities.py @@ -13,6 +13,7 @@ from datums import DATUMS # assumes this is in same folder from pynmeagps import ( + area, bearing, ecef2llh, haversine, @@ -44,21 +45,21 @@ f" {LAT2}, {LON2} using default WGS84 datum...", ) dist = haversine(LAT1, LON1, LAT2, LON2) -print(f"Distance: {dist} km") +print(f"Distance: {dist:.4f} km") print( f"\nFind planar distance between {LAT1}, {LON1} and", f" {LAT3}, {LON3} using default WGS84 datum...", ) dist = planar(LAT1, LON1, LAT3, LON3) -print(f"Distance: {dist} m") +print(f"Distance: {dist:.4f} m") print( f"\nFind bearing between {LAT1}, {LON1} and", f" {LAT2}, {LON2} using default WGS84 datum...", ) brng = bearing(LAT1, LON1, LAT2, LON2) -print(f"Bearing: {brng} degrees") +print(f"Bearing: {brng:.4f} degrees") X, Y, Z = 3822566.3113, -144427.5123, 5086857.1208 print(f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to geodetic using default WGS84 datum...") @@ -82,7 +83,14 @@ f"{LAT2}, {LON2} using alternate {DATUM} ({ellipsoid}) datum...", ) dist = haversine(LAT1, LON1, LAT2, LON2, a / 1000) -print(f"Distance: {dist} km") +print(f"Distance: {dist:.4f} km") + +print( + f"\nFind spherical area bound by {LAT1}, {LON1} and", + f"{LAT2}, {LON2}...", +) +bbarea = area(LAT1, LON1, LAT2, LON2) +print(f"Area: {bbarea:.4f} km²") print( f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to", diff --git a/pyproject.toml b/pyproject.toml index 0fec835..f87f106 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pynmeagps" authors = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] description = "NMEA protocol parser and generator" -version = "1.0.39" +version = "1.0.40" license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.8" diff --git a/src/pynmeagps/__init__.py b/src/pynmeagps/__init__.py index d26456d..b6cc82d 100644 --- a/src/pynmeagps/__init__.py +++ b/src/pynmeagps/__init__.py @@ -20,6 +20,6 @@ from pynmeagps.nmeareader import NMEAReader from pynmeagps.nmeatypes_core import * from pynmeagps.nmeatypes_get import * -from pynmeagps.socket_stream import SocketStream +from pynmeagps.socket_wrapper import SocketWrapper version = __version__ diff --git a/src/pynmeagps/_version.py b/src/pynmeagps/_version.py index ba2fa63..5b76a94 100644 --- a/src/pynmeagps/_version.py +++ b/src/pynmeagps/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.0.39" +__version__ = "1.0.40" diff --git a/src/pynmeagps/nmeahelpers.py b/src/pynmeagps/nmeahelpers.py index 982215a..33165da 100644 --- a/src/pynmeagps/nmeahelpers.py +++ b/src/pynmeagps/nmeahelpers.py @@ -546,6 +546,29 @@ def bearing(lat1: float, lon1: float, lat2: float, lon2: float) -> float: return brng +def area( + lat1: float, + lon1: float, + lat2: float, + lon2: float, + radius: int = WGS84_SMAJ_AXIS / 1000, +) -> float: + """ + Calculate spherical area bounded by two coordinates. + + :param float lat1: lat1 + :param float lon1: lon1 + :param float lat2: lat2 + :param float lon2: lon2 + :param float radius: radius in km (Earth = 6378.137 km) + :return: area in km² + :rtype: float + """ + + phi1, phi2 = [c * pi / 180 for c in (lat1, lat2)] + return pow(radius, 2) * pi * abs(sin(phi1) - sin(phi2)) * abs(lon1 - lon2) / 180 + + def get_gpswnotow(dat: datetime) -> tuple: """ Get GPS Week number (Wno) and Time of Week (Tow) diff --git a/src/pynmeagps/nmeareader.py b/src/pynmeagps/nmeareader.py index ecd1d2d..f18acbb 100644 --- a/src/pynmeagps/nmeareader.py +++ b/src/pynmeagps/nmeareader.py @@ -35,7 +35,7 @@ VALCKSUM, VALMSGID, ) -from pynmeagps.socket_stream import SocketStream +from pynmeagps.socket_wrapper import SocketWrapper class NMEAReader: @@ -68,7 +68,7 @@ def __init__( # pylint: disable=too-many-arguments if isinstance(stream, socket): - self._stream = SocketStream(stream, bufsize=bufsize) + self._stream = SocketWrapper(stream, bufsize=bufsize) else: self._stream = stream if msgmode not in (0, 1, 2): diff --git a/src/pynmeagps/nmeatypes_core.py b/src/pynmeagps/nmeatypes_core.py index 233058b..7147218 100644 --- a/src/pynmeagps/nmeatypes_core.py +++ b/src/pynmeagps/nmeatypes_core.py @@ -13,18 +13,39 @@ from datetime import datetime INPUT = 1 +"""Input message type""" OUTPUT = 0 +"""Output message type""" GET = 0 +"""GET (receive, response) message types""" SET = 1 +"""SET (command) message types""" POLL = 2 +"""POLL (query) message types""" +SETPOLL = 3 +"""SETPOLL (SET or POLL) message types""" VALNONE = 0 +"""Do not validate checksum or msgid""" VALCKSUM = 1 +"""Validate checksum""" VALMSGID = 2 -ERR_IGNORE = 0 -ERR_LOG = 1 +"""Validate message id""" +NMEA_PROTOCOL = 1 +"""NMEA Protocol""" +UBX_PROTOCOL = 2 +"""UBX Protocol""" +RTCM3_PROTOCOL = 4 +"""RTCM3 Protocol""" ERR_RAISE = 2 +"""Raise error and quit""" +ERR_LOG = 1 +"""Log errors""" +ERR_IGNORE = 0 +"""Ignore errors""" + # proprietary messages where msgId is first element of payload: PROP_MSGIDS = ("FEC", "UBX", "TNL", "ASHR", "GPPADV") +"""Proprietary message prefixes""" GNSSLIST = { 0: "GPS", @@ -36,13 +57,27 @@ 6: "GLONASS", 7: "NAVIC", } +"""GNSS code""" +FIXTYPE_GGA = { + 0: "NO FIX", + 1: "3D", + 2: "3D", + 4: "RTK FIXED", + 5: "RTK FLOAT", + 6: "DR", +} +"""Fix type from GGA""" GPSEPOCH0 = datetime(1980, 1, 6) +"""GPS epoch base date""" # Geodetic datum spheroid values: # WGS84, ETRS89, EPSG4326 WGS84 = "WGS_84" +"""WGS84 datum descriptor""" WGS84_SMAJ_AXIS = 6378137.0 # semi-major axis +"""WGS84 semi-major axis""" WGS84_FLATTENING = 298.257223563 # flattening +"""WGS84 flattening""" # *************************************************** # THESE ARE THE NMEA PROTOCOL PAYLOAD ATTRIBUTE TYPES diff --git a/src/pynmeagps/socket_stream.py b/src/pynmeagps/socket_wrapper.py similarity index 98% rename from src/pynmeagps/socket_stream.py rename to src/pynmeagps/socket_wrapper.py index c8573e3..73b0182 100644 --- a/src/pynmeagps/socket_stream.py +++ b/src/pynmeagps/socket_wrapper.py @@ -1,5 +1,5 @@ """ -socket_stream class. +socket_wrapper class. A skeleton socket wrapper which provides basic stream-like read(bytes) and readline() methods. @@ -19,7 +19,7 @@ from socket import socket -class SocketStream: +class SocketWrapper: """ socket stream class. """ diff --git a/tests/test_static.py b/tests/test_static.py index 448021d..5cb980f 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -14,6 +14,7 @@ from pynmeagps import NMEAMessage, NMEAMessageError, NMEAReader, NMEATypeError from pynmeagps.nmeahelpers import ( + area, bearing, calc_checksum, date2str, @@ -488,6 +489,16 @@ def testbearing(self): res = bearing(-12.645, 34.867, -34.1745, 48.27846) self.assertAlmostEqual(res, 152.70835788275326, 4) + def testarea(self): + res = area(51.23, -2.41, 53.205, -2.34) + self.assertAlmostEqual(res, 1049.5657, 4) + res = area(53.48280729, -2.24225376, 53.46814647, -2.20192543) + self.assertAlmostEqual(res, 4.3606, 4) + res = area(53.69865772, -2.68269539, 53.22939103, -1.39218885) + self.assertAlmostEqual(res, 4467.6282, 4) + res = area(-12.645, 34.867, -34.1745, 48.27846) + self.assertAlmostEqual(res, 3264291.8230, 4) + def testgpsweek(self): dats = [ (2023, 1, 1),