diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cde81b0..5f4e526 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/geodepy/angles.py b/geodepy/angles.py index 49af0d8..87f93ef 100644 --- a/geodepy/angles.py +++ b/geodepy/angles.py @@ -533,33 +533,38 @@ class DMSAngle(object): """ Class for working with angles in Degrees, Minutes and Seconds format """ - def __init__(self, degree, minute=0, second=0.0): + def __init__(self, degree, minute=0, second=0.0, positive=None): """ :param degree: Angle: whole degrees component (floats truncated) Alt: formatted string '±DDD MM SS.SSS' + :type degree: float | str :param minute: Angle: whole minutes component (floats truncated) + :type minute: float :param second: Angle: seconds component (floats preserved) + :type second: float + :param positive: Optional. True is positive, False is negative. Evaluated from deg/min/sec where None + :type positive: bool """ + # evaluate sign + if positive is False or str(degree)[0] == '-': + self.positive = False + else: + self.positive = True + + # check sign provided for minute and second where positive not provided and degree is int == 0 + if degree == 0 and positive is None: + if minute < 0: + self.positive = False + elif second < 0: + self.positive = False + # Convert formatted string 'DDD MM SS.SSS' to DMSAngle if type(degree) == str: str_pts = degree.split() degree = int(str_pts[0]) minute = int(str_pts[1]) second = float(str_pts[2]) - # Set sign of object based on sign of any variable - if degree == 0: - if str(degree)[0] == '-': - self.positive = False - elif minute < 0: - self.positive = False - elif second < 0: - self.positive = False - else: - self.positive = True - elif degree > 0: - self.positive = True - else: # degree < 0 - self.positive = False + self.degree = abs(int(degree)) self.minute = abs(int(minute)) self.second = abs(second) @@ -656,6 +661,9 @@ def __round__(self, n=None): else: return -DMSAngle(self.degree, self.minute, round(self.second, n)) + def __mod__(self, other): + return dec2dms(self.dec() % other) + def rad(self): """ Convert to Radians @@ -734,28 +742,33 @@ class DDMAngle(object): """ Class for working with angles in Degrees, Decimal Minutes format """ - def __init__(self, degree, minute=0.0): + def __init__(self, degree, minute=0.0, positive=None): """ :param degree: Angle: whole degrees component (floats truncated) - :param minute: Angle:minutes component (floats preserved) + :type degree: float | str + :param minute: Angle: minutes component (floats preserved) + :type minute: float + :param positive: Optional. True is positive, False is negative. Evaluated from deg/min/sec where None + :type positive: bool """ + + # evaluate sign + if positive is False or str(degree)[0] == '-': + self.positive = False + else: + self.positive = True + + # check sign provided for minute where positive not provided and degree is int == 0 + if degree == 0 and positive is None: + if minute < 0: + self.positive = False + # Convert formatted string 'DDD MM.MMMM' to DDMAngle if type(degree) == str: str_pts = degree.split(' ') degree = int(str_pts[0]) minute = float(str_pts[1]) - # Set sign of object based on sign of any variable - if degree == 0: - if str(degree)[0] == '-': - self.positive = False - elif minute < 0: - self.positive = False - else: - self.positive = True - elif degree > 0: - self.positive = True - else: # degree < 0 - self.positive = False + self.degree = abs(int(degree)) self.minute = abs(minute) @@ -849,6 +862,10 @@ def __round__(self, n=None): else: return DDMAngle(-self.degree, -round(self.minute, n)) + def __mod__(self, other): + return dec2ddm(self.dec() % other) + + def rad(self): """ Convert to Radians @@ -1001,8 +1018,8 @@ def dec2dms(dec): """ minute, second = divmod(abs(dec) * 3600, 60) degree, minute = divmod(minute, 60) - return (DMSAngle(degree, minute, second) if dec >= 0 - else DMSAngle(-degree, minute, second)) + return (DMSAngle(degree, minute, second, positive=True) if dec >= 0 + else DMSAngle(degree, minute, second, positive=False)) def dec2ddm(dec): @@ -1016,7 +1033,7 @@ def dec2ddm(dec): minute, second = divmod(abs(dec) * 3600, 60) degree, minute = divmod(minute, 60) minute = minute + (second / 60) - return DDMAngle(degree, minute) if dec >= 0 else DDMAngle(-degree, minute) + return DDMAngle(degree, minute, positive=True) if dec >= 0 else DDMAngle(degree, minute, positive=False) # Functions converting from Hewlett-Packard (HP) format to other formats @@ -1102,8 +1119,8 @@ def hp2dms(hp): """ degmin, second = divmod(abs(hp) * 1000, 10) degree, minute = divmod(degmin, 100) - return (DMSAngle(degree, minute, second * 10) if hp >= 0 - else DMSAngle(-degree, minute, second * 10)) + return (DMSAngle(degree, minute, second * 10, positive=True) if hp >= 0 + else DMSAngle(degree, minute, second * 10, positive=False)) def hp2ddm(hp): @@ -1117,7 +1134,7 @@ def hp2ddm(hp): degmin, second = divmod(abs(hp) * 1000, 10) degree, minute = divmod(degmin, 100) minute = minute + (second / 6) - return DDMAngle(degree, minute) if hp >= 0 else DDMAngle(-degree, minute) + return DDMAngle(degree, minute, positive=True) if hp >= 0 else DDMAngle(degree, minute, positive=False) # Functions converting from Gradians format to other formats diff --git a/geodepy/tests/test_angles.py b/geodepy/tests/test_angles.py index e8d94fb..8fd9574 100644 --- a/geodepy/tests/test_angles.py +++ b/geodepy/tests/test_angles.py @@ -12,77 +12,88 @@ dd2sec, angular_typecheck) rad_exs = [radians(123.74875), radians(12.575), radians(-12.575), - radians(0.0525), radians(0.005)] + radians(0.0525), radians(0.005), radians(-0.005)] dec_ex = 123.74875 dec_ex2 = 12.575 dec_ex3 = -12.575 dec_ex4 = 0.0525 dec_ex5 = 0.005 -dec_exs = [dec_ex, dec_ex2, dec_ex3, dec_ex4, dec_ex5] +dec_ex6 = -0.005 +dec_exs = [dec_ex, dec_ex2, dec_ex3, dec_ex4, dec_ex5, dec_ex6] deca_ex = DECAngle(123.74875) deca_ex2 = DECAngle(12.575) deca_ex3 = DECAngle(-12.575) deca_ex4 = DECAngle(0.0525) deca_ex5 = DECAngle(0.005) -deca_exs = [deca_ex, deca_ex2, deca_ex3, deca_ex4, deca_ex5] +deca_ex6 = DECAngle(-0.005) +deca_exs = [deca_ex, deca_ex2, deca_ex3, deca_ex4, deca_ex5, deca_ex6] hp_ex = 123.44555 hp_ex2 = 12.3430 hp_ex3 = -12.3430 hp_ex4 = 0.0309 hp_ex5 = 0.0018 -hp_exs = [hp_ex, hp_ex2, hp_ex3, hp_ex4, hp_ex5] +hp_ex6 = -0.0018 +hp_exs = [hp_ex, hp_ex2, hp_ex3, hp_ex4, hp_ex5, hp_ex6] hpa_ex = HPAngle(123.44555) hpa_ex2 = HPAngle(12.3430) hpa_ex3 = HPAngle(-12.3430) hpa_ex4 = HPAngle(0.0309) hpa_ex5 = HPAngle(0.0018) -hpa_exs = [hpa_ex, hpa_ex2, hpa_ex3, hpa_ex4, hpa_ex5] +hpa_ex6 = HPAngle(-0.0018) +hpa_exs = [hpa_ex, hpa_ex2, hpa_ex3, hpa_ex4, hpa_ex5, hpa_ex6] dms_ex = DMSAngle(123, 44, 55.5) dms_ex2 = DMSAngle(12, 34, 30) dms_ex3 = DMSAngle(-12, -34, -30) dms_ex4 = DMSAngle(0, 3, 9) dms_ex5 = DMSAngle(0, 0, 18) -dms_exs = [dms_ex, dms_ex2, dms_ex3, dms_ex4, dms_ex5] +# dms_ex6 = DMSAngle(-0, 0, -18) +dms_ex6 = DMSAngle(0, 0, 18, positive=False) +dms_exs = [dms_ex, dms_ex2, dms_ex3, dms_ex4, dms_ex5, dms_ex6] dms_str = '123 44 55.5' dms_str2 = '12 34 30' dms_str3 = '-12 34 30' dms_str4 = '0 3 9' dms_str5 = '0 0 18' -dms_strs = [dms_str, dms_str2, dms_str3, dms_str4, dms_str5] +dms_str6 = '-0 0 18' +dms_strs = [dms_str, dms_str2, dms_str3, dms_str4, dms_str5, dms_str6] ddm_ex = DDMAngle(123, 44.925) ddm_ex2 = DDMAngle(12, 34.5) ddm_ex3 = DDMAngle(-12, -34.5) ddm_ex4 = DDMAngle(0, 3.15) ddm_ex5 = DDMAngle(0, 0.3) -ddm_exs = [ddm_ex, ddm_ex2, ddm_ex3, ddm_ex4, ddm_ex5] +ddm_ex6 = DDMAngle(0, 0.3, positive=False) +ddm_exs = [ddm_ex, ddm_ex2, ddm_ex3, ddm_ex4, ddm_ex5, ddm_ex6] ddm_str = '123 44.925' ddm_str2 = '12 34.5' ddm_str3 = '-12 34.5' ddm_str4 = '0 3.15' ddm_str5 = '0 0.3' -ddm_strs = [ddm_str, ddm_str2, ddm_str3, ddm_str4, ddm_str5] +ddm_str6 = '-0 0.3' +ddm_strs = [ddm_str, ddm_str2, ddm_str3, ddm_str4, ddm_str5, ddm_str6] gon_ex = 137.4986111111111 gon_ex2 = 13.97222222222222 gon_ex3 = -13.97222222222222 gon_ex4 = 0.05833333333333333 gon_ex5 = 0.00555555555555555 -gon_exs = [gon_ex, gon_ex2, gon_ex3, gon_ex4, gon_ex5] +gon_ex6 = -0.00555555555555555 +gon_exs = [gon_ex, gon_ex2, gon_ex3, gon_ex4, gon_ex5, gon_ex6] gona_ex = GONAngle(137.4986111111111) gona_ex2 = GONAngle(13.97222222222222) gona_ex3 = GONAngle(-13.97222222222222) gona_ex4 = GONAngle(0.05833333333333333) gona_ex5 = GONAngle(0.00555555555555555) -gona_exs = [gona_ex, gona_ex2, gona_ex3, gona_ex4, gona_ex5] +gona_ex6 = GONAngle(-0.00555555555555555) +gona_exs = [gona_ex, gona_ex2, gona_ex3, gona_ex4, gona_ex5, gona_ex6] class TestConvert(unittest.TestCase): @@ -350,7 +361,7 @@ def test_DMSAngle(self): self.assertTrue(DMSAngle(1, 2, -3).positive) self.assertFalse(DMSAngle(0, -1, 2).positive) self.assertFalse(DMSAngle(0, 0, -3).positive) - self.assertTrue(DMSAngle(-0, 1, 2).positive) + self.assertFalse(DMSAngle(0, 1, 2, positive=False).positive) self.assertFalse(DMSAngle(-0.0, 1, 2).positive) self.assertEqual(repr(dms_ex), '{DMSAngle: +123d 44m 55.5s}') self.assertEqual(repr(dms_ex3), '{DMSAngle: -12d 34m 30s}') @@ -434,7 +445,7 @@ def test_DDMAngle(self): self.assertTrue(DDMAngle(1, -2).positive) self.assertTrue(DDMAngle(1, 2).positive) self.assertFalse(DDMAngle(0, -1).positive) - self.assertTrue(DDMAngle(-0, 1).positive) + self.assertFalse(DDMAngle(0, 1, positive=False).positive) self.assertFalse(DDMAngle(-0.0, 1).positive) self.assertEqual(repr(ddm_ex), '{DDMAngle: +123d 44.925m}') self.assertEqual(repr(ddm_ex3), '{DDMAngle: -12d 34.5m}') diff --git a/geodepy/tests/test_convert.py b/geodepy/tests/test_convert.py index 8f96ae2..21c3bd5 100644 --- a/geodepy/tests/test_convert.py +++ b/geodepy/tests/test_convert.py @@ -72,11 +72,11 @@ def test_geo_grid_transform_interoperability(self): dtype='S4,i4,f8,f8', names=['site', 'zone', 'east', 'north']) - geoed_grid = np.array(list(grid2geo(*x) for x in test_grid_coords[['zone', 'east', 'north']])) + geoed_grid = np.array([grid2geo(*x) for x in test_grid_coords[['zone', 'east', 'north']]]) np.testing.assert_almost_equal(geoed_grid[:, :2], hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist())), decimal=8) - gridded_geo = np.stack(geo2grid(*x) for x in hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist()))) + gridded_geo = np.stack([geo2grid(*x) for x in hp2dec_v(np.array(test_geo_coords[['lat', 'lon']].tolist()))]) np.testing.assert_almost_equal(gridded_geo[:, 2:4].astype(float), np.array(test_grid_coords[['east', 'north']].tolist()), decimal=3) @@ -93,11 +93,14 @@ def test_geo_grid_transform_interoperability_isg(self): dtype='S4,i4,f8,f8', names=['site', 'zone', 'east', 'north']) - geoed_grid = np.array(list(grid2geo(*x, ellipsoid=ans, prj=isg) for x in test_grid_coords[['zone', 'east', 'north']])) + geoed_grid = np.array([grid2geo(*x, ellipsoid=ans, prj=isg) + for x in test_grid_coords[['zone', 'east', 'north']]]) np.testing.assert_almost_equal(geoed_grid[:, :2], np.array(test_geo_coords[['lat', 'lon']].tolist()), decimal=8) - gridded_geo = np.stack(geo2grid(*x, ellipsoid=ans, prj=isg) for x in np.array(test_geo_coords[['lat', 'lon']].tolist())) + gridded_geo = np.stack([geo2grid(*x, ellipsoid=ans, prj=isg) + for x in np.array(test_geo_coords[['lat', 'lon']].tolist()) + ]) np.testing.assert_almost_equal(gridded_geo[:, 2:4].astype(float), np.array(test_grid_coords[['east', 'north']].tolist()), decimal=3) diff --git a/geodepy/transform.py b/geodepy/transform.py index f37ff60..c41c772 100644 --- a/geodepy/transform.py +++ b/geodepy/transform.py @@ -58,9 +58,9 @@ def conform7(x, y, z, trans, vcv=None): # Conformal Transform Eq xyz_after = translation + scale * rot_xyz # Convert Vector to Separate Variables - xtrans = float(xyz_after[0]) - ytrans = float(xyz_after[1]) - ztrans = float(xyz_after[2]) + xtrans = float(xyz_after[0][0]) + ytrans = float(xyz_after[1][0]) + ztrans = float(xyz_after[2][0]) # Transformation uncertainty propagation # Adapted from Harvey B.R. (1998) Practical least squares and statistics for surveyors,