diff --git a/aprs/__init__.py b/aprs/__init__.py index 3ca2afd7718ba898a7653f726469f950cdd706f9..8fcaab90233acf16df6b23bfd0032078fedf54f3 100644 --- a/aprs/__init__.py +++ b/aprs/__init__.py @@ -6,4 +6,4 @@ __license__ = 'Creative Commons Attribution 3.0 Unported License' from .aprs import APRS -from .util import * +import util diff --git a/aprs/decimaldegrees.py b/aprs/decimaldegrees.py new file mode 100644 index 0000000000000000000000000000000000000000..5b1975a09856d8a7977333eec1333c3d079e6249 --- /dev/null +++ b/aprs/decimaldegrees.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +""" +PyDecimalDegrees - geographic coordinates conversion utility. + +Copyright (C) 2006-2013 by Mateusz Åoskot <mateusz@loskot.net> +Copyright (C) 2010-2013 by Evan Wheeler <ewheeler@unicef.org> + +This file is part of PyDecimalDegrees module. + +This software is provided 'as-is', without any express or implied warranty. +In no event will the authors be held liable for any damages arising from +the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it freely, +subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +DESCRIPTION + +DecimalDegrees module provides functions to convert between +degrees/minutes/seconds and decimal degrees. + +Original source distribution: +http://mateusz.loskot.net/software/gis/pydecimaldegrees/ + +Inspired by Walter Mankowski's Geo::Coordinates::DecimalDegrees module +for Perl, originally located in CPAN Archives: +http://search.cpan.org/~waltman/Geo-Coordinates-DecimalDegrees-0.05/ + +doctest examples are based following coordinates: +DMS: 121 8' 6" +DM: 121 8.1' +DD: 121.135 + +To run doctest units just execut this module script as follows +(-v instructs Python to run script in verbose mode): + +$ python decimaldegrees.py [-v] + +""" +__revision__ = "$Revision: 1.1 $" + +import decimal as libdecimal +from decimal import Decimal as D + + +def decimal2dms(decimal_degrees): + """ Converts a floating point number of degrees to the equivalent + number of degrees, minutes, and seconds, which are returned + as a 3-element tuple of decimals. If 'decimal_degrees' is negative, + only degrees (1st element of returned tuple) will be negative, + minutes (2nd element) and seconds (3rd element) will always be positive. + + Example: + >>> decimal2dms(121.135) + (Decimal('121'), Decimal('8'), Decimal('6.000')) + >>> decimal2dms(-121.135) + (Decimal('-121'), Decimal('8'), Decimal('6.000')) + + """ + + degrees = D(int(decimal_degrees)) + decimal_minutes = libdecimal.getcontext().multiply(\ + (D(str(decimal_degrees)) - degrees).copy_abs(), D(60)) + minutes = D(int(decimal_minutes)) + seconds = libdecimal.getcontext().multiply(\ + (decimal_minutes - minutes), D(60)) + return (degrees, minutes, seconds) + + +def decimal2dm(decimal_degrees): + """ Converts a floating point number of degrees to the equivalent + number of degrees and minutes, which are returned as a 2-element tuple of decimals. + If 'decimal_degrees' is negative, only degrees (1st element of returned tuple) + will be negative, minutes (2nd element) will always be positive. + + Example: + >>> decimal2dm(121.135) + (Decimal('121'), Decimal('8.100')) + >>> decimal2dm(-121.135) + (Decimal('-121'), Decimal('8.100')) + + """ + + degrees = D(int(decimal_degrees)) + minutes = libdecimal.getcontext().multiply(\ + (D(str(decimal_degrees)) - degrees).copy_abs(), D(60)) + return (degrees, minutes) + + +def dms2decimal(degrees, minutes, seconds): + """ Converts degrees, minutes, and seconds to the equivalent + number of decimal degrees. If parameter 'degrees' is negative, + then returned decimal-degrees will also be negative. + + NOTE: this method returns a decimal.Decimal + + Example: + >>> dms2decimal(121, 8, 6) + Decimal('121.135') + >>> dms2decimal(-121, 8, 6) + Decimal('-121.135') + + """ + + decimal = D(0) + deg = D(str(degrees)) + min = libdecimal.getcontext().divide(D(str(minutes)), D(60)) + sec = libdecimal.getcontext().divide(D(str(seconds)), D(3600)) + if (degrees >= D(0)): + decimal = deg + min + sec + else: + decimal = deg - min - sec + + return libdecimal.getcontext().normalize(decimal) + + +def dm2decimal(degrees, minutes): + """ Converts degrees and minutes to the equivalent number of decimal + degrees. If parameter 'degrees' is negative, then returned decimal-degrees + will also be negative. + + Example: + >>> dm2decimal(121, 8.1) + Decimal('121.135') + >>> dm2decimal(-121, 8.1) + Decimal('-121.135') + + """ + return dms2decimal(degrees, minutes, 0) + + +# Run doctest +def _test(): + import doctest, decimaldegrees + return doctest.testmod(decimaldegrees) + +if __name__ == "__main__": + _test() + diff --git a/aprs/util.py b/aprs/util.py index db1f158596da3e9cf73012e55a96ae943f56d3ca..ebf0c30e41f2d7e9ae41a0fc7c3ef0a651d1fc60 100755 --- a/aprs/util.py +++ b/aprs/util.py @@ -1,44 +1,61 @@ #!/usr/bin/env python +import decimaldegrees + + # http://stackoverflow.com/questions/2056750/lat-long-to-minutes-and-seconds -def lat_deg_to_dms(coord): +def dec2dm_lat(dec): """Converts DecDeg to APRS Coord format. See: http://ember2ash.com/lat.htm - """ - degrees = int(coord) - mindeg = abs(coord - degrees) * 60 - minutes = int(mindeg) + Example: + >>> test_lat = 37.7418096 + >>> aprs_lat = dec2dm_lat(test_lat) + >>> aprs_lat + '3744.51N' + """ + dm = decimaldegrees.decimal2dm(dec) - secdeg = (mindeg - minutes) * 60 - seconds = int(secdeg) + deg = dm[0] + abs_deg = abs(deg) - if not degrees == abs(degrees): + if not deg == abs_deg: suffix = 'S' else: suffix = 'N' - ddmmss = [degrees, minutes, '.', seconds, suffix] - return ''.join([str(c) for c in ddmmss]) + return ''.join([str(abs_deg), "%.2f" % dm[1], suffix]) -def lng_deg_to_dms(coord): +def dec2dm_lng(dec): """Converts DecDeg to APRS Coord format. See: http://ember2ash.com/lat.htm - """ - degrees = int(coord) - mindeg = abs(coord - degrees) * 60 - minutes = int(mindeg) + Example: + >>> test_lng = -122.38833 + >>> aprs_lng = dec2dm_lng(test_lng) + >>> aprs_lng + '12223.30W' + """ + dm = decimaldegrees.decimal2dm(dec) - secdeg = (mindeg - minutes) * 60 - seconds = int(secdeg) + deg = dm[0] + abs_deg = abs(deg) - if not degrees == abs(degrees): + if not deg == abs_deg: suffix = 'W' else: suffix = 'E' - ddmmss = [abs(degrees), minutes, '.', seconds, suffix] - return ''.join([str(c) for c in ddmmss]) + return ''.join([str(abs_deg), "%.2f" % dm[1], suffix]) + + +def run_doctest(): + import doctest + import util + return doctest.testmod(util) + + +if __name__ == '__main__': + run_doctest() diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..7a59f7614a259d9c143353b0a593de1aa5b5788a --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + + +import unittest +import logging +import logging.handlers + +from .context import aprs + + +ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +NUMBERS = '0123456789' +POSITIVE_NUMBERS = NUMBERS[1:] +ALPHANUM = ''.join([ALPHABET, NUMBERS]) + + +class APRSUtilTest(unittest.TestCase): + """Tests for Python APRS Utils.""" + + logger = logging.getLogger('aprs.util.tests') + logger.addHandler(logging.StreamHandler()) + + def test_latitude(self): + """Test Decimal to APRS Latitude conversion. + + Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf + -- + Latitude is expressed as a fixed 8-character field, in degrees and + decimal minutes (to two decimal places), followed by the letter N for + north or S for south. Latitude degrees are in the range 00 to 90. + Latitude minutes are expressed as whole minutes and hundredths of a + minute, separated by a decimal point. + + For example: + + 4903.50N is 49 degrees 3 minutes 30 seconds north. + + In generic format examples, the latitude is shown as the 8-character + string ddmm.hhN (i.e. degrees, minutes and hundredths of a minute + north). + """ + test_lat = 37.7418096 + aprs_lat = aprs.util.dec2dm_lat(test_lat) + self.logger.debug("aprs_lat=%s" % aprs_lat) + + lat_deg = int(aprs_lat.split('.')[0][:1]) + lat_hsec = aprs_lat.split('.')[1] + + self.assertTrue(len(aprs_lat) == 8) + self.assertTrue(lat_deg >= 00) + self.assertTrue(lat_deg <= 90) + self.assertTrue(aprs_lat.endswith('N')) + + + def test_longitude(self): + """Test Decimal to APRS Longitude conversion. + + Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf + -- + Longitude is expressed as a fixed 9-character field, in degrees and + decimal minutes (to two decimal places), followed by the letter E for + east or W for west. + + Longitude degrees are in the range 000 to 180. Longitude minutes are + expressed as whole minutes and hundredths of a minute, separated by a + decimal point. + + For example: + + 07201.75W is 72 degrees 1 minute 45 seconds west. + + In generic format examples, the longitude is shown as the 9-character + string dddmm.hhW (i.e. degrees, minutes and hundredths of a minute + west). + """ + test_lng = -122.38833 + aprs_lng = aprs.util.dec2dm_lng(test_lng) + self.logger.debug("aprs_lng=%s" % aprs_lng) + + lng_deg = int(aprs_lng.split('.')[0][:2]) + lng_hsec = aprs_lng.split('.')[1] + + self.assertTrue(len(aprs_lng) == 9) + self.assertTrue(lng_deg >= 000) + self.assertTrue(lng_deg <= 180) + self.assertTrue(aprs_lng.endswith('W')) + + +if __name__ == '__main__': + unittest.main()