diff --git a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py index 596aa858cf..13aa359671 100644 --- a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py +++ b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py @@ -234,7 +234,7 @@ def step_impl(context, guid, number): site = util.assert_guid(IfcStore.file, guid) util.assert_type(site, "IfcSite") ref = util.assert_attribute(site, "RefLongitude") - number = ifcopenshell.util.geolocation.dd2dms(number, use_ms=(len(ref) == 4)) + number = ifcopenshell.util.geolocation.dd2dms(number, use_us=(len(ref) == 4)) util.assert_attribute(site, "RefLongitude", number) @@ -244,7 +244,7 @@ def step_impl(context, guid, number): site = util.assert_guid(IfcStore.file, guid) util.assert_type(site, "IfcSite") ref = util.assert_attribute(site, "RefLatitude") - number = ifcopenshell.util.geolocation.dd2dms(number, use_ms=(len(ref) == 4)) + number = ifcopenshell.util.geolocation.dd2dms(number, use_us=(len(ref) == 4)) util.assert_attribute(site, "RefLatitude", number) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index 2855bfde5c..45ae344bca 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -23,6 +23,7 @@ import ifcopenshell.util.unit import ifcopenshell.util.element import ifcopenshell.util.placement from typing import NamedTuple, Optional, Union +from decimal import Decimal, ROUND_HALF_UP MatrixType = ifcopenshell.util.placement.MatrixType @@ -39,39 +40,54 @@ class HelmertTransformation(NamedTuple): factor_z: float -def dms2dd(degrees: int, minutes: int, seconds: int, ms: int = 0) -> float: - """Convert degrees, minutes, and (milli)seconds to decimal degrees +def dms2dd(degrees: int, minutes: int, seconds: int, us: int = 0) -> float: + """Convert degrees, minutes, and (micro)seconds to decimal degrees + All components must be either positive or negative. :param degrees: The degrees component :param minutes: The minutes component :param seconds: The seconds component - :param ms: The milliseconds component + :param us: The microseconds component :return: The angle in decimal degrees. """ - dd = float(degrees) + float(minutes) / 60.0 + float(seconds) / (3600.0) + float(ms / 3600000000.0) - return dd + all_positive_or_zero = degrees >= 0 and minutes >= 0 and seconds >= 0 and us >= 0 + all_negative_or_zero = degrees <= 0 and minutes <= 0 and seconds <= 0 and us <= 0 + assert all_positive_or_zero or all_negative_or_zero + return degrees + minutes / 60.0 + seconds / 3600.0 + us / 3600000000.0 -def dd2dms(dd: float, use_ms: bool = False) -> Union[tuple[float, float, float, float], tuple[float, float, float]]: - """Convert decimal degrees to degrees, minutes, and (milli)seconds format +def dd2dms(dd: float, use_us: bool = False) -> Union[tuple[int, int, int, int], tuple[int, int, float]]: + """Convert decimal degrees to degrees, minutes, and (micro)seconds format :param dd: The decimal degrees - :param use_ms: True if to include milliseconds and false otherwise. Defaults to false. - :return: The angle in a tuple of either 3 or 4 values, being degrees, - minutes, seconds, and optionally milliseconds. + :param use_us: True if to include microseconds and false otherwise. Defaults to false. + :return: The angle in a tuple of either 3 or 4 values, + 4 values: integer number of degrees, integer number of minutes, integer number of seconds and integer number of microseconds + 3 values: integer number of degrees, integer number of minutes, and a float number for seconds + :note: the tuple follows the format of IfcCompoundPlaneAngleMeasure. Namely all of its components are either positive or negative. """ - dd = float(dd) - sign = 1 if dd >= 0 else -1 - dd = abs(dd) - if use_ms: - seconds, ms = divmod(dd * 60 * 60 * 1000000, 1000000) - minutes, seconds = divmod(dd * 60 * 60, 60) - degrees, minutes = divmod(minutes, 60) - if dd < 0: - degrees = -degrees - if use_ms: - return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign, int(ms) * sign) - return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign) + dd_decimal = Decimal(str(dd)) + + degrees = int(dd_decimal) + degrees_decimal = Decimal(degrees) + + fractional_part = dd_decimal - degrees_decimal + + minutes_decimal = fractional_part * Decimal(60) + minutes = int(minutes_decimal) + minutes_decimal_int = Decimal(minutes) + + seconds_decimal = (minutes_decimal - minutes_decimal_int) * Decimal(60) + + if use_us: + seconds = int(seconds_decimal) + seconds_decimal_int = Decimal(seconds) + microseconds_decimal = (seconds_decimal - seconds_decimal_int) * Decimal(1000000) + microseconds = int(microseconds_decimal.quantize(Decimal(1), rounding=ROUND_HALF_UP)) + return (degrees, minutes, seconds, microseconds) + else: + seconds_float = float(seconds_decimal) + return (degrees, minutes, seconds_float) def xyz2enh( diff --git a/src/ifcopenshell-python/test/util/test_geolocation.py b/src/ifcopenshell-python/test/util/test_geolocation.py index 532f971b42..59f70fcd7a 100644 --- a/src/ifcopenshell-python/test/util/test_geolocation.py +++ b/src/ifcopenshell-python/test/util/test_geolocation.py @@ -468,3 +468,29 @@ class TestAngle2YAxis(test.bootstrap.IFC4): assert np.allclose(subject.angle2yaxis(45), (-a, a)) assert np.allclose(subject.angle2yaxis(-135), (a, -a)) assert np.allclose(subject.angle2yaxis(135), (-a, -a)) + + +class TestDMS2DDandDD2DMS(test.bootstrap.IFC4): + def test_dms2dd_and_dd2dms(self): + test_cases_3tuple = [ + (35.41, (35, 24, 36.0)), + (-116.89, (-116, -53, -24.0)), + ] + test_cases_4tuple = [ + (40.431389, (40, 25, 53, 400)), + (-4.248056, (-4, -14, -53, -1600)), + (-35.401389, (-35, -24, -5, -400)), + (148.981667, (148, 58, 54, 1200)), + ] + + for dd, dms in test_cases_3tuple: + d, m, s = subject.dd2dms(dd) + assert (d, m, s) == dms + dd_converted = subject.dms2dd(dms[0], dms[1], dms[2]) + assert dd_converted == dd + + for dd, dms in test_cases_4tuple: + d, m, s, us = subject.dd2dms(dd, use_us=True) + assert (d, m, s, us) == dms + dd_converted = subject.dms2dd(dms[0], dms[1], dms[2], dms[3]) + assert dd_converted == dd