mirror of
https://github.com/dh1tw/pyhamtools.git
synced 2026-01-06 08:39:59 +01:00
support for 4, 6, 8 char precision maidenhead locator conversions
# fixes bug report #30
This commit is contained in:
parent
84d88faf69
commit
5ec3461d03
|
|
@ -1,6 +1,14 @@
|
|||
Changelog
|
||||
---------
|
||||
|
||||
PyHamtools 0.10.0
|
||||
================
|
||||
|
||||
01. June 2024
|
||||
|
||||
* full support for 4, 6, 8 characters Maidenhead locator conversions
|
||||
|
||||
|
||||
PyHamtools 0.9.1
|
||||
================
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ from datetime import datetime, timezone
|
|||
|
||||
import ephem
|
||||
|
||||
def latlong_to_locator (latitude, longitude):
|
||||
def latlong_to_locator (latitude, longitude, precision=6):
|
||||
"""converts WGS84 coordinates into the corresponding Maidenhead Locator
|
||||
|
||||
Args:
|
||||
latitude (float): Latitude
|
||||
longitude (float): Longitude
|
||||
precision (int): 4,6,8 chars (default 6)
|
||||
|
||||
Returns:
|
||||
string: Maidenhead locator
|
||||
|
|
@ -32,35 +33,54 @@ def latlong_to_locator (latitude, longitude):
|
|||
|
||||
"""
|
||||
|
||||
if precision < 4 or precision ==5 or precision == 7 or precision > 8:
|
||||
return ValueError
|
||||
|
||||
if longitude >= 180 or longitude <= -180:
|
||||
raise ValueError
|
||||
|
||||
if latitude >= 90 or latitude <= -90:
|
||||
raise ValueError
|
||||
|
||||
longitude += 180;
|
||||
latitude +=90;
|
||||
longitude +=180
|
||||
latitude +=90
|
||||
|
||||
locator = chr(ord('A') + int(longitude / 20))
|
||||
locator += chr(ord('A') + int(latitude / 10))
|
||||
locator += chr(ord('0') + int((longitude % 20) / 2))
|
||||
locator += chr(ord('0') + int(latitude % 10))
|
||||
locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24)))
|
||||
locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24)))
|
||||
# copied & adapted from github.com/space-physics/maidenhead
|
||||
A = ord('A')
|
||||
a = divmod(longitude, 20)
|
||||
b = divmod(latitude, 10)
|
||||
locator = chr(A + int(a[0])) + chr(A + int(b[0]))
|
||||
lon = a[1] / 2.0
|
||||
lat = b[1]
|
||||
i = 1
|
||||
|
||||
while i < precision/2:
|
||||
i += 1
|
||||
a = divmod(lon, 1)
|
||||
b = divmod(lat, 1)
|
||||
if not (i % 2):
|
||||
locator += str(int(a[0])) + str(int(b[0]))
|
||||
lon = 24 * a[1]
|
||||
lat = 24 * b[1]
|
||||
else:
|
||||
locator += chr(A + int(a[0])) + chr(A + int(b[0]))
|
||||
lon = 10 * a[1]
|
||||
lat = 10 * b[1]
|
||||
|
||||
return locator
|
||||
|
||||
def locator_to_latlong (locator):
|
||||
def locator_to_latlong (locator, center=True):
|
||||
"""converts Maidenhead locator in the corresponding WGS84 coordinates
|
||||
|
||||
Args:
|
||||
locator (string): Locator, either 4 or 6 characters
|
||||
locator (string): Locator, either 4, 6 or 8 characters
|
||||
center (bool): Center of (sub)square. By default True. If False, the south/western corner will be returned
|
||||
|
||||
Returns:
|
||||
tuple (float, float): Latitude, Longitude
|
||||
|
||||
Raises:
|
||||
ValueError: When called with wrong or invalid input arg
|
||||
ValueError: When called with wrong or invalid Maidenhead locator string
|
||||
TypeError: When arg is not a string
|
||||
|
||||
Example:
|
||||
|
|
@ -79,7 +99,7 @@ def locator_to_latlong (locator):
|
|||
|
||||
locator = locator.upper()
|
||||
|
||||
if len(locator) == 5 or len(locator) < 4:
|
||||
if len(locator) < 4 or len(locator) == 5 or len(locator) == 7:
|
||||
raise ValueError
|
||||
|
||||
if ord(locator[0]) > ord('R') or ord(locator[0]) < ord('A'):
|
||||
|
|
@ -100,23 +120,44 @@ def locator_to_latlong (locator):
|
|||
if ord (locator[5]) > ord('X') or ord(locator[5]) < ord('A'):
|
||||
raise ValueError
|
||||
|
||||
if len(locator) == 8:
|
||||
if ord(locator[6]) > ord('9') or ord(locator[6]) < ord('0'):
|
||||
raise ValueError
|
||||
if ord (locator[7]) > ord('9') or ord(locator[7]) < ord('0'):
|
||||
raise ValueError
|
||||
|
||||
longitude = (ord(locator[0]) - ord('A')) * 20 - 180
|
||||
latitude = (ord(locator[1]) - ord('A')) * 10 - 90
|
||||
longitude += (ord(locator[2]) - ord('0')) * 2
|
||||
latitude += (ord(locator[3]) - ord('0'))
|
||||
latitude += (ord(locator[3]) - ord('0')) * 1
|
||||
|
||||
if len(locator) == 6:
|
||||
longitude += ((ord(locator[4])) - ord('A')) * (2 / 24)
|
||||
latitude += ((ord(locator[5])) - ord('A')) * (1 / 24)
|
||||
if len(locator) == 4:
|
||||
|
||||
# move to center of subsquare
|
||||
longitude += 1 / 24
|
||||
latitude += 0.5 / 24
|
||||
if center:
|
||||
longitude += 2 / 2
|
||||
latitude += 1.0 / 2
|
||||
|
||||
elif len(locator) == 6:
|
||||
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
|
||||
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
|
||||
|
||||
if center:
|
||||
longitude += 5.0 / 60 / 2
|
||||
latitude += 2.5 / 60 / 2
|
||||
|
||||
elif len(locator) == 8:
|
||||
longitude += (ord(locator[4]) - ord('A')) * 5.0 / 60
|
||||
latitude += (ord(locator[5]) - ord('A')) * 2.5 / 60
|
||||
|
||||
longitude += int(locator[6]) * 5.0 / 600
|
||||
latitude += int(locator[7]) * 2.5 / 600
|
||||
|
||||
if center:
|
||||
longitude += 5.0 / 600 / 2
|
||||
latitude += 2.5 / 600 / 2
|
||||
|
||||
else:
|
||||
# move to center of square
|
||||
longitude += 1;
|
||||
latitude += 0.5;
|
||||
raise ValueError
|
||||
|
||||
return latitude, longitude
|
||||
|
||||
|
|
@ -125,14 +166,14 @@ def calculate_distance(locator1, locator2):
|
|||
"""calculates the (shortpath) distance between two Maidenhead locators
|
||||
|
||||
Args:
|
||||
locator1 (string): Locator, either 4 or 6 characters
|
||||
locator2 (string): Locator, either 4 or 6 characters
|
||||
locator1 (string): Locator, either 4, 6 or 8 characters
|
||||
locator2 (string): Locator, either 4, 6 or 8 characters
|
||||
|
||||
Returns:
|
||||
float: Distance in km
|
||||
|
||||
Raises:
|
||||
ValueError: When called with wrong or invalid input arg
|
||||
ValueError: When called with wrong or invalid maidenhead locator strings
|
||||
AttributeError: When args are not a string
|
||||
|
||||
Example:
|
||||
|
|
@ -142,6 +183,9 @@ def calculate_distance(locator1, locator2):
|
|||
>>> calculate_distance("JN48QM", "QF67bf")
|
||||
16466.413
|
||||
|
||||
Note:
|
||||
Distances is calculated between the centers of the (sub) squares
|
||||
|
||||
"""
|
||||
|
||||
R = 6371 #earh radius
|
||||
|
|
@ -160,15 +204,15 @@ def calculate_distance(locator1, locator2):
|
|||
c = 2 * atan2(sqrt(a), sqrt(1-a))
|
||||
d = R * c #distance in km
|
||||
|
||||
return d;
|
||||
return d
|
||||
|
||||
|
||||
def calculate_distance_longpath(locator1, locator2):
|
||||
"""calculates the (longpath) distance between two Maidenhead locators
|
||||
|
||||
Args:
|
||||
locator1 (string): Locator, either 4 or 6 characters
|
||||
locator2 (string): Locator, either 4 or 6 characters
|
||||
locator1 (string): Locator, either 4, 6 or 8 characters
|
||||
locator2 (string): Locator, either 4, 6 or 8 characters
|
||||
|
||||
Returns:
|
||||
float: Distance in km
|
||||
|
|
@ -184,6 +228,8 @@ def calculate_distance_longpath(locator1, locator2):
|
|||
>>> calculate_distance_longpath("JN48QM", "QF67bf")
|
||||
23541.5867
|
||||
|
||||
Note:
|
||||
Distance is calculated between the centers of the (sub) squares
|
||||
"""
|
||||
|
||||
c = 40008 #[km] earth circumference
|
||||
|
|
@ -196,8 +242,8 @@ def calculate_heading(locator1, locator2):
|
|||
"""calculates the heading from the first to the second locator
|
||||
|
||||
Args:
|
||||
locator1 (string): Locator, either 4 or 6 characters
|
||||
locator2 (string): Locator, either 4 or 6 characters
|
||||
locator1 (string): Locator, either 4, 6 or 8 characters
|
||||
locator2 (string): Locator, either 4, 6 or 6 characters
|
||||
|
||||
Returns:
|
||||
float: Heading in deg
|
||||
|
|
@ -213,6 +259,9 @@ def calculate_heading(locator1, locator2):
|
|||
>>> calculate_heading("JN48QM", "QF67bf")
|
||||
74.3136
|
||||
|
||||
Note:
|
||||
Heading is calculated between the centers of the (sub) squares
|
||||
|
||||
"""
|
||||
|
||||
lat1, long1 = locator_to_latlong(locator1)
|
||||
|
|
@ -236,8 +285,8 @@ def calculate_heading_longpath(locator1, locator2):
|
|||
"""calculates the heading from the first to the second locator (long path)
|
||||
|
||||
Args:
|
||||
locator1 (string): Locator, either 4 or 6 characters
|
||||
locator2 (string): Locator, either 4 or 6 characters
|
||||
locator1 (string): Locator, either 4, 6 or 8 characters
|
||||
locator2 (string): Locator, either 4, 6 or 8 characters
|
||||
|
||||
Returns:
|
||||
float: Long path heading in deg
|
||||
|
|
@ -253,6 +302,9 @@ def calculate_heading_longpath(locator1, locator2):
|
|||
>>> calculate_heading_longpath("JN48QM", "QF67bf")
|
||||
254.3136
|
||||
|
||||
Note:
|
||||
Distance is calculated between the centers of the (sub) squares
|
||||
|
||||
"""
|
||||
|
||||
heading = calculate_heading(locator1, locator2)
|
||||
|
|
@ -265,7 +317,7 @@ def calculate_sunrise_sunset(locator, calc_date=None):
|
|||
"""calculates the next sunset and sunrise for a Maidenhead locator at a give date & time
|
||||
|
||||
Args:
|
||||
locator1 (string): Maidenhead Locator, either 4 or 6 characters
|
||||
locator1 (string): Maidenhead Locator, either 4, 6 or 8 characters
|
||||
calc_date (datetime, optional): Starting datetime for the calculations (UTC)
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
VERSION = (0, 9, 1)
|
||||
VERSION = (0, 10, 0)
|
||||
__release__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:]
|
||||
__version__ = '.'.join((str(VERSION[0]), str(VERSION[1])))
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ pytest==4.6.11; python_version=='3.6'
|
|||
pytest-blockage>=0.2.2
|
||||
pytest-localserver>=0.5
|
||||
pytest-cov>=2.12
|
||||
maidenhead==1.7.0
|
||||
|
|
@ -14,7 +14,12 @@ class Test_calculate_distance():
|
|||
|
||||
assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1
|
||||
assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1
|
||||
assert abs(calculate_distance("JN48QM", "QF67bf") - 16467) < 1
|
||||
assert abs(calculate_distance("JN48QM", "QF67BF") - 16467) < 1
|
||||
assert abs(calculate_distance("JN48QM84", "QF67BF84") - 16467) < 1
|
||||
assert abs(calculate_distance("JN48QM84", "QF67BF") - 16464) < 1
|
||||
assert abs(calculate_distance("JN48QM84", "QF67") - 16506) < 1
|
||||
assert abs(calculate_distance("JN48QM", "QF67") - 16508) < 1
|
||||
assert abs(calculate_distance("JN48", "QF67") - 16535) < 1
|
||||
|
||||
def test_calculate_distance_invalid_inputs(self):
|
||||
with pytest.raises(AttributeError):
|
||||
|
|
|
|||
|
|
@ -8,10 +8,20 @@ class Test_latlong_to_locator():
|
|||
assert latlong_to_locator(-89.97916, -179.95833) == "AA00AA"
|
||||
assert latlong_to_locator(89.97916, 179.9583) == "RR99XX"
|
||||
|
||||
def test_latlong_to_locator_normal_case(self):
|
||||
def test_latlong_to_locator_4chars_precision(self):
|
||||
|
||||
assert latlong_to_locator(48.52083, 9.3750000, precision=4) == "JN48"
|
||||
assert latlong_to_locator(39.222916, -86.45416, 4) == "EM69"
|
||||
|
||||
def test_latlong_to_locator_6chars_precision(self):
|
||||
|
||||
assert latlong_to_locator(48.52083, 9.3750000) == "JN48QM"
|
||||
assert latlong_to_locator(48.5, 9.0) == "JN48MM" #center of the square
|
||||
assert latlong_to_locator(39.222916, -86.45416, 6) == "EM69SF"
|
||||
|
||||
def test_latlong_to_locator_8chars_precision(self):
|
||||
assert latlong_to_locator(48.51760, 9.40345, precision=8) == "JN48QM84"
|
||||
assert latlong_to_locator(39.222916, -86.45416, 4) == "EM69SF53"
|
||||
|
||||
def test_latlong_to_locator_invalid_characters(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import pytest
|
||||
import maidenhead
|
||||
from pyhamtools.locator import locator_to_latlong
|
||||
from pyhamtools.consts import LookupConventions as const
|
||||
|
||||
|
||||
class Test_locator_to_latlong():
|
||||
|
||||
def test_locator_to_latlong_edge_cases(self):
|
||||
def test_locator_to_latlong_min_max_cases(self):
|
||||
latitude, longitude = locator_to_latlong("AA00AA")
|
||||
assert abs(latitude + 89.97916) < 0.00001
|
||||
assert abs(longitude +179.95833) < 0.0001
|
||||
|
|
@ -13,23 +15,71 @@ class Test_locator_to_latlong():
|
|||
assert abs(latitude - 89.97916) < 0.00001
|
||||
assert abs(longitude - 179.9583) < 0.0001
|
||||
|
||||
def test_locator_to_latlong_normal_case(self):
|
||||
|
||||
latitude, longitude = locator_to_latlong("JN48QM")
|
||||
assert abs(latitude - 48.52083) < 0.00001
|
||||
assert abs(longitude - 9.3750000) < 0.0001
|
||||
def test_locator_to_latlong_4chars_precision(self):
|
||||
|
||||
latitude, longitude = locator_to_latlong("JN48")
|
||||
assert abs(latitude - 48.5) < 0.001
|
||||
assert abs(longitude - 9.000) < 0.001
|
||||
assert abs(latitude - 48.5) < 0.1
|
||||
assert abs(longitude - 9.0) < 0.1
|
||||
|
||||
def test_locator_to_latlong_mixed_signs(self):
|
||||
latitude, longitude = locator_to_latlong("JN48", center=False)
|
||||
assert abs(latitude - 48) < 0.1
|
||||
assert abs(longitude - 8) < 0.1
|
||||
|
||||
def test_locator_to_latlong_6chars_precision(self):
|
||||
latitude, longitude = locator_to_latlong("JN48QM")
|
||||
assert abs(latitude - 48.52083) < 0.00001
|
||||
assert abs(longitude - 9.37500) < 0.00001
|
||||
|
||||
def test_locator_to_latlong_8chars(self):
|
||||
|
||||
latitude, longitude = locator_to_latlong("JN48QM84")
|
||||
assert abs(latitude - 48.51875) < 0.00001
|
||||
assert abs(longitude - 9.40416) < 0.00001
|
||||
|
||||
latitude, longitude = locator_to_latlong("EM69SF53")
|
||||
assert abs(latitude - 39.222916) < 0.00001
|
||||
assert abs(longitude + 86.45416) < 0.00001
|
||||
|
||||
def test_locator_to_latlong_consistency_checks_6chars_lower_left_corner(self):
|
||||
|
||||
latitude_4, longitude_4 = locator_to_latlong("JN48", center=False)
|
||||
latitude_6, longitude_6 = locator_to_latlong("JN48AA", center=False)
|
||||
|
||||
assert latitude_4 == latitude_6
|
||||
assert longitude_4 == longitude_6
|
||||
|
||||
def test_locator_to_latlong_consistency_checks_8chars_lower_left_corner(self):
|
||||
|
||||
latitude_6, longitude_6 = locator_to_latlong("JN48AA", center=False)
|
||||
latitude_8, longitude_8 = locator_to_latlong("JN48AA00", center=False)
|
||||
|
||||
assert latitude_6 == latitude_8
|
||||
assert longitude_6 == longitude_8
|
||||
|
||||
def test_locator_to_latlong_consistency_checks_against_maidenhead(self):
|
||||
|
||||
locs = ["JN48", "EM69", "JN48QM", "EM69SF", "AA00AA", "RR99XX", "JN48QM84", "EM69SF53"]
|
||||
|
||||
# lower left (south/east) corner
|
||||
for loc in locs:
|
||||
lat, lon = locator_to_latlong(loc, center=False)
|
||||
lat_m, lon_m = maidenhead.to_location(loc)
|
||||
assert abs(lat - lat_m) < 0.00001
|
||||
assert abs(lon - lon_m) < 0.00001
|
||||
|
||||
# center of square
|
||||
for loc in locs:
|
||||
lat, lon = locator_to_latlong(loc) # default: center=True
|
||||
lat_m, lon_m = maidenhead.to_location(loc, center=True)
|
||||
assert abs(lat - lat_m) < 0.1
|
||||
assert abs(lon - lon_m) < 0.1
|
||||
|
||||
def test_locator_to_latlong_upper_lower_chars(self):
|
||||
|
||||
latitude, longitude = locator_to_latlong("Jn48qM")
|
||||
assert abs(latitude - 48.52083) < 0.00001
|
||||
assert abs(longitude - 9.3750000) < 0.0001
|
||||
|
||||
|
||||
def test_locator_to_latlong_wrong_amount_of_characters(self):
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
|
@ -43,12 +93,27 @@ class Test_locator_to_latlong():
|
|||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("JN8Q")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("JN8QM1")
|
||||
|
||||
def test_locator_to_latlong_invalid_characters(self):
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("21XM99")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("48")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("JNJN")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("JN4848")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("JN48QMaa")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
latitude, longitude = locator_to_latlong("****")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue