Skip to content

Commit 476a8d3

Browse files
authored
Merge pull request #158 from nicgowans/master
Improve handling of negative values for DMSAngles and DDMAngle classes
2 parents 6af0d51 + ca6587d commit 476a8d3

File tree

5 files changed

+87
-56
lines changed

5 files changed

+87
-56
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Python ${{ matrix.python-version }}
1717
uses: actions/setup-python@v2
1818
with:
19-
python-version: '3.7'
19+
python-version: '3.13'
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip

geodepy/angles.py

+52-35
Original file line numberDiff line numberDiff line change
@@ -533,33 +533,38 @@ class DMSAngle(object):
533533
"""
534534
Class for working with angles in Degrees, Minutes and Seconds format
535535
"""
536-
def __init__(self, degree, minute=0, second=0.0):
536+
def __init__(self, degree, minute=0, second=0.0, positive=None):
537537
"""
538538
:param degree: Angle: whole degrees component (floats truncated)
539539
Alt: formatted string '±DDD MM SS.SSS'
540+
:type degree: float | str
540541
:param minute: Angle: whole minutes component (floats truncated)
542+
:type minute: float
541543
:param second: Angle: seconds component (floats preserved)
544+
:type second: float
545+
:param positive: Optional. True is positive, False is negative. Evaluated from deg/min/sec where None
546+
:type positive: bool
542547
"""
548+
# evaluate sign
549+
if positive is False or str(degree)[0] == '-':
550+
self.positive = False
551+
else:
552+
self.positive = True
553+
554+
# check sign provided for minute and second where positive not provided and degree is int == 0
555+
if degree == 0 and positive is None:
556+
if minute < 0:
557+
self.positive = False
558+
elif second < 0:
559+
self.positive = False
560+
543561
# Convert formatted string 'DDD MM SS.SSS' to DMSAngle
544562
if type(degree) == str:
545563
str_pts = degree.split()
546564
degree = int(str_pts[0])
547565
minute = int(str_pts[1])
548566
second = float(str_pts[2])
549-
# Set sign of object based on sign of any variable
550-
if degree == 0:
551-
if str(degree)[0] == '-':
552-
self.positive = False
553-
elif minute < 0:
554-
self.positive = False
555-
elif second < 0:
556-
self.positive = False
557-
else:
558-
self.positive = True
559-
elif degree > 0:
560-
self.positive = True
561-
else: # degree < 0
562-
self.positive = False
567+
563568
self.degree = abs(int(degree))
564569
self.minute = abs(int(minute))
565570
self.second = abs(second)
@@ -656,6 +661,9 @@ def __round__(self, n=None):
656661
else:
657662
return -DMSAngle(self.degree, self.minute, round(self.second, n))
658663

664+
def __mod__(self, other):
665+
return dec2dms(self.dec() % other)
666+
659667
def rad(self):
660668
"""
661669
Convert to Radians
@@ -734,28 +742,33 @@ class DDMAngle(object):
734742
"""
735743
Class for working with angles in Degrees, Decimal Minutes format
736744
"""
737-
def __init__(self, degree, minute=0.0):
745+
def __init__(self, degree, minute=0.0, positive=None):
738746
"""
739747
:param degree: Angle: whole degrees component (floats truncated)
740-
:param minute: Angle:minutes component (floats preserved)
748+
:type degree: float | str
749+
:param minute: Angle: minutes component (floats preserved)
750+
:type minute: float
751+
:param positive: Optional. True is positive, False is negative. Evaluated from deg/min/sec where None
752+
:type positive: bool
741753
"""
754+
755+
# evaluate sign
756+
if positive is False or str(degree)[0] == '-':
757+
self.positive = False
758+
else:
759+
self.positive = True
760+
761+
# check sign provided for minute where positive not provided and degree is int == 0
762+
if degree == 0 and positive is None:
763+
if minute < 0:
764+
self.positive = False
765+
742766
# Convert formatted string 'DDD MM.MMMM' to DDMAngle
743767
if type(degree) == str:
744768
str_pts = degree.split(' ')
745769
degree = int(str_pts[0])
746770
minute = float(str_pts[1])
747-
# Set sign of object based on sign of any variable
748-
if degree == 0:
749-
if str(degree)[0] == '-':
750-
self.positive = False
751-
elif minute < 0:
752-
self.positive = False
753-
else:
754-
self.positive = True
755-
elif degree > 0:
756-
self.positive = True
757-
else: # degree < 0
758-
self.positive = False
771+
759772
self.degree = abs(int(degree))
760773
self.minute = abs(minute)
761774

@@ -849,6 +862,10 @@ def __round__(self, n=None):
849862
else:
850863
return DDMAngle(-self.degree, -round(self.minute, n))
851864

865+
def __mod__(self, other):
866+
return dec2ddm(self.dec() % other)
867+
868+
852869
def rad(self):
853870
"""
854871
Convert to Radians
@@ -1001,8 +1018,8 @@ def dec2dms(dec):
10011018
"""
10021019
minute, second = divmod(abs(dec) * 3600, 60)
10031020
degree, minute = divmod(minute, 60)
1004-
return (DMSAngle(degree, minute, second) if dec >= 0
1005-
else DMSAngle(-degree, minute, second))
1021+
return (DMSAngle(degree, minute, second, positive=True) if dec >= 0
1022+
else DMSAngle(degree, minute, second, positive=False))
10061023

10071024

10081025
def dec2ddm(dec):
@@ -1016,7 +1033,7 @@ def dec2ddm(dec):
10161033
minute, second = divmod(abs(dec) * 3600, 60)
10171034
degree, minute = divmod(minute, 60)
10181035
minute = minute + (second / 60)
1019-
return DDMAngle(degree, minute) if dec >= 0 else DDMAngle(-degree, minute)
1036+
return DDMAngle(degree, minute, positive=True) if dec >= 0 else DDMAngle(degree, minute, positive=False)
10201037

10211038

10221039
# Functions converting from Hewlett-Packard (HP) format to other formats
@@ -1102,8 +1119,8 @@ def hp2dms(hp):
11021119
"""
11031120
degmin, second = divmod(abs(hp) * 1000, 10)
11041121
degree, minute = divmod(degmin, 100)
1105-
return (DMSAngle(degree, minute, second * 10) if hp >= 0
1106-
else DMSAngle(-degree, minute, second * 10))
1122+
return (DMSAngle(degree, minute, second * 10, positive=True) if hp >= 0
1123+
else DMSAngle(degree, minute, second * 10, positive=False))
11071124

11081125

11091126
def hp2ddm(hp):
@@ -1117,7 +1134,7 @@ def hp2ddm(hp):
11171134
degmin, second = divmod(abs(hp) * 1000, 10)
11181135
degree, minute = divmod(degmin, 100)
11191136
minute = minute + (second / 6)
1120-
return DDMAngle(degree, minute) if hp >= 0 else DDMAngle(-degree, minute)
1137+
return DDMAngle(degree, minute, positive=True) if hp >= 0 else DDMAngle(degree, minute, positive=False)
11211138

11221139

11231140
# Functions converting from Gradians format to other formats

geodepy/tests/test_angles.py

+24-13
Original file line numberDiff line numberDiff line change
@@ -12,77 +12,88 @@
1212
dd2sec, angular_typecheck)
1313

1414
rad_exs = [radians(123.74875), radians(12.575), radians(-12.575),
15-
radians(0.0525), radians(0.005)]
15+
radians(0.0525), radians(0.005), radians(-0.005)]
1616

1717
dec_ex = 123.74875
1818
dec_ex2 = 12.575
1919
dec_ex3 = -12.575
2020
dec_ex4 = 0.0525
2121
dec_ex5 = 0.005
22-
dec_exs = [dec_ex, dec_ex2, dec_ex3, dec_ex4, dec_ex5]
22+
dec_ex6 = -0.005
23+
dec_exs = [dec_ex, dec_ex2, dec_ex3, dec_ex4, dec_ex5, dec_ex6]
2324

2425
deca_ex = DECAngle(123.74875)
2526
deca_ex2 = DECAngle(12.575)
2627
deca_ex3 = DECAngle(-12.575)
2728
deca_ex4 = DECAngle(0.0525)
2829
deca_ex5 = DECAngle(0.005)
29-
deca_exs = [deca_ex, deca_ex2, deca_ex3, deca_ex4, deca_ex5]
30+
deca_ex6 = DECAngle(-0.005)
31+
deca_exs = [deca_ex, deca_ex2, deca_ex3, deca_ex4, deca_ex5, deca_ex6]
3032

3133
hp_ex = 123.44555
3234
hp_ex2 = 12.3430
3335
hp_ex3 = -12.3430
3436
hp_ex4 = 0.0309
3537
hp_ex5 = 0.0018
36-
hp_exs = [hp_ex, hp_ex2, hp_ex3, hp_ex4, hp_ex5]
38+
hp_ex6 = -0.0018
39+
hp_exs = [hp_ex, hp_ex2, hp_ex3, hp_ex4, hp_ex5, hp_ex6]
3740

3841
hpa_ex = HPAngle(123.44555)
3942
hpa_ex2 = HPAngle(12.3430)
4043
hpa_ex3 = HPAngle(-12.3430)
4144
hpa_ex4 = HPAngle(0.0309)
4245
hpa_ex5 = HPAngle(0.0018)
43-
hpa_exs = [hpa_ex, hpa_ex2, hpa_ex3, hpa_ex4, hpa_ex5]
46+
hpa_ex6 = HPAngle(-0.0018)
47+
hpa_exs = [hpa_ex, hpa_ex2, hpa_ex3, hpa_ex4, hpa_ex5, hpa_ex6]
4448

4549
dms_ex = DMSAngle(123, 44, 55.5)
4650
dms_ex2 = DMSAngle(12, 34, 30)
4751
dms_ex3 = DMSAngle(-12, -34, -30)
4852
dms_ex4 = DMSAngle(0, 3, 9)
4953
dms_ex5 = DMSAngle(0, 0, 18)
50-
dms_exs = [dms_ex, dms_ex2, dms_ex3, dms_ex4, dms_ex5]
54+
# dms_ex6 = DMSAngle(-0, 0, -18)
55+
dms_ex6 = DMSAngle(0, 0, 18, positive=False)
56+
dms_exs = [dms_ex, dms_ex2, dms_ex3, dms_ex4, dms_ex5, dms_ex6]
5157

5258
dms_str = '123 44 55.5'
5359
dms_str2 = '12 34 30'
5460
dms_str3 = '-12 34 30'
5561
dms_str4 = '0 3 9'
5662
dms_str5 = '0 0 18'
57-
dms_strs = [dms_str, dms_str2, dms_str3, dms_str4, dms_str5]
63+
dms_str6 = '-0 0 18'
64+
dms_strs = [dms_str, dms_str2, dms_str3, dms_str4, dms_str5, dms_str6]
5865

5966
ddm_ex = DDMAngle(123, 44.925)
6067
ddm_ex2 = DDMAngle(12, 34.5)
6168
ddm_ex3 = DDMAngle(-12, -34.5)
6269
ddm_ex4 = DDMAngle(0, 3.15)
6370
ddm_ex5 = DDMAngle(0, 0.3)
64-
ddm_exs = [ddm_ex, ddm_ex2, ddm_ex3, ddm_ex4, ddm_ex5]
71+
ddm_ex6 = DDMAngle(0, 0.3, positive=False)
72+
ddm_exs = [ddm_ex, ddm_ex2, ddm_ex3, ddm_ex4, ddm_ex5, ddm_ex6]
6573

6674
ddm_str = '123 44.925'
6775
ddm_str2 = '12 34.5'
6876
ddm_str3 = '-12 34.5'
6977
ddm_str4 = '0 3.15'
7078
ddm_str5 = '0 0.3'
71-
ddm_strs = [ddm_str, ddm_str2, ddm_str3, ddm_str4, ddm_str5]
79+
ddm_str6 = '-0 0.3'
80+
ddm_strs = [ddm_str, ddm_str2, ddm_str3, ddm_str4, ddm_str5, ddm_str6]
7281

7382
gon_ex = 137.4986111111111
7483
gon_ex2 = 13.97222222222222
7584
gon_ex3 = -13.97222222222222
7685
gon_ex4 = 0.05833333333333333
7786
gon_ex5 = 0.00555555555555555
78-
gon_exs = [gon_ex, gon_ex2, gon_ex3, gon_ex4, gon_ex5]
87+
gon_ex6 = -0.00555555555555555
88+
gon_exs = [gon_ex, gon_ex2, gon_ex3, gon_ex4, gon_ex5, gon_ex6]
7989

8090
gona_ex = GONAngle(137.4986111111111)
8191
gona_ex2 = GONAngle(13.97222222222222)
8292
gona_ex3 = GONAngle(-13.97222222222222)
8393
gona_ex4 = GONAngle(0.05833333333333333)
8494
gona_ex5 = GONAngle(0.00555555555555555)
85-
gona_exs = [gona_ex, gona_ex2, gona_ex3, gona_ex4, gona_ex5]
95+
gona_ex6 = GONAngle(-0.00555555555555555)
96+
gona_exs = [gona_ex, gona_ex2, gona_ex3, gona_ex4, gona_ex5, gona_ex6]
8697

8798

8899
class TestConvert(unittest.TestCase):
@@ -350,7 +361,7 @@ def test_DMSAngle(self):
350361
self.assertTrue(DMSAngle(1, 2, -3).positive)
351362
self.assertFalse(DMSAngle(0, -1, 2).positive)
352363
self.assertFalse(DMSAngle(0, 0, -3).positive)
353-
self.assertTrue(DMSAngle(-0, 1, 2).positive)
364+
self.assertFalse(DMSAngle(0, 1, 2, positive=False).positive)
354365
self.assertFalse(DMSAngle(-0.0, 1, 2).positive)
355366
self.assertEqual(repr(dms_ex), '{DMSAngle: +123d 44m 55.5s}')
356367
self.assertEqual(repr(dms_ex3), '{DMSAngle: -12d 34m 30s}')
@@ -434,7 +445,7 @@ def test_DDMAngle(self):
434445
self.assertTrue(DDMAngle(1, -2).positive)
435446
self.assertTrue(DDMAngle(1, 2).positive)
436447
self.assertFalse(DDMAngle(0, -1).positive)
437-
self.assertTrue(DDMAngle(-0, 1).positive)
448+
self.assertFalse(DDMAngle(0, 1, positive=False).positive)
438449
self.assertFalse(DDMAngle(-0.0, 1).positive)
439450
self.assertEqual(repr(ddm_ex), '{DDMAngle: +123d 44.925m}')
440451
self.assertEqual(repr(ddm_ex3), '{DDMAngle: -12d 34.5m}')

geodepy/tests/test_convert.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ def test_geo_grid_transform_interoperability(self):
7272
dtype='S4,i4,f8,f8',
7373
names=['site', 'zone', 'east', 'north'])
7474

75-
geoed_grid = np.array(list(grid2geo(*x) for x in test_grid_coords[['zone', 'east', 'north']]))
75+
geoed_grid = np.array([grid2geo(*x) for x in test_grid_coords[['zone', 'east', 'north']]])
7676
np.testing.assert_almost_equal(geoed_grid[:, :2], hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist())),
7777
decimal=8)
7878

79-
gridded_geo = np.stack(geo2grid(*x) for x in hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist())))
79+
gridded_geo = np.stack([geo2grid(*x) for x in hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist()))])
8080
np.testing.assert_almost_equal(gridded_geo[:, 2:4].astype(float),
8181
np.array(test_grid_coords[['east', 'north']].tolist()),
8282
decimal=3)
@@ -93,11 +93,14 @@ def test_geo_grid_transform_interoperability_isg(self):
9393
dtype='S4,i4,f8,f8',
9494
names=['site', 'zone', 'east', 'north'])
9595

96-
geoed_grid = np.array(list(grid2geo(*x, ellipsoid=ans, prj=isg) for x in test_grid_coords[['zone', 'east', 'north']]))
96+
geoed_grid = np.array([grid2geo(*x, ellipsoid=ans, prj=isg)
97+
for x in test_grid_coords[['zone', 'east', 'north']]])
9798
np.testing.assert_almost_equal(geoed_grid[:, :2], np.array(test_geo_coords[['lat', 'lon']].tolist()),
9899
decimal=8)
99100

100-
gridded_geo = np.stack(geo2grid(*x, ellipsoid=ans, prj=isg) for x in np.array(test_geo_coords[['lat', 'lon']].tolist()))
101+
gridded_geo = np.stack([geo2grid(*x, ellipsoid=ans, prj=isg)
102+
for x in np.array(test_geo_coords[['lat', 'lon']].tolist())
103+
])
101104
np.testing.assert_almost_equal(gridded_geo[:, 2:4].astype(float),
102105
np.array(test_grid_coords[['east', 'north']].tolist()),
103106
decimal=3)

geodepy/transform.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def conform7(x, y, z, trans, vcv=None):
5858
# Conformal Transform Eq
5959
xyz_after = translation + scale * rot_xyz
6060
# Convert Vector to Separate Variables
61-
xtrans = float(xyz_after[0])
62-
ytrans = float(xyz_after[1])
63-
ztrans = float(xyz_after[2])
61+
xtrans = float(xyz_after[0][0])
62+
ytrans = float(xyz_after[1][0])
63+
ztrans = float(xyz_after[2][0])
6464

6565
# Transformation uncertainty propagation
6666
# Adapted from Harvey B.R. (1998) Practical least squares and statistics for surveyors,

0 commit comments

Comments
 (0)