From 7405acbd5909e72bfbe137a30a1ac97a2d78e154 Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 11:14:54 -0400 Subject: [PATCH 1/6] Enhance location data with accuracy metrics --- android/src/toga_android/hardware/location.py | 10 +++++++++- cocoa/src/toga_cocoa/hardware/location.py | 5 ++++- iOS/src/toga_iOS/hardware/location.py | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/android/src/toga_android/hardware/location.py b/android/src/toga_android/hardware/location.py index 2f0dce7f51..e5b723f4a9 100644 --- a/android/src/toga_android/hardware/location.py +++ b/android/src/toga_android/hardware/location.py @@ -24,11 +24,19 @@ def toga_location(location): else: altitude = None - return { + result = { "location": latlng, "altitude": altitude, } + if location.hasAccuracy(): + result["accuracy"] = location.getAccuracy() + + if location.hasVerticalAccuracy(): + result["vertical_accuracy"] = location.getVerticalAccuracyMeters() + + return result + class TogaLocationConsumer(dynamic_proxy(Consumer)): def __init__(self, impl, result): diff --git a/cocoa/src/toga_cocoa/hardware/location.py b/cocoa/src/toga_cocoa/hardware/location.py index 74bf8bf51e..9774051455 100644 --- a/cocoa/src/toga_cocoa/hardware/location.py +++ b/cocoa/src/toga_cocoa/hardware/location.py @@ -19,12 +19,15 @@ def toga_location(location): # A vertical accuracy that non-positive indicates altitude is invalid. if location.verticalAccuracy > 0.0: altitude = location.altitude + vertical_accuracy = location.verticalAccuracy else: altitude = None - + vertical_accuracy = None return { "location": latlng, "altitude": altitude, + "horizontalAccuracy": location.horizontalAccuracy, + "verticalAccuracy": vertical_accuracy, } diff --git a/iOS/src/toga_iOS/hardware/location.py b/iOS/src/toga_iOS/hardware/location.py index bdf94500de..3ae3c37e04 100644 --- a/iOS/src/toga_iOS/hardware/location.py +++ b/iOS/src/toga_iOS/hardware/location.py @@ -22,12 +22,15 @@ def toga_location(location): # A vertical accuracy that non-positive indicates altitude is invalid. if location.verticalAccuracy > 0.0: altitude = location.altitude + vertical_accuracy = location.verticalAccuracy else: altitude = None - + vertical_accuracy = None return { "location": latlng, "altitude": altitude, + "horizontalAccuracy": location.horizontalAccuracy, + "verticalAccuracy": vertical_accuracy, } From e128f3fa3ba5b345699d912d4fbc01b30d8f99ab Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 11:37:45 -0400 Subject: [PATCH 2/6] Added and fields to the Location class to represent measurement accuracies --- core/src/toga/types.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/src/toga/types.py b/core/src/toga/types.py index ca0fca5822..c0c19d864b 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -24,8 +24,22 @@ class LatLng(NamedTuple): #: Longitude lng: float + #: Horizontal accuracy in meters + horizontal_accuracy: float | None = None + + #: Vertical accuracy in meters + vertical_accuracy: float | None = None + def __str__(self) -> str: - return f"({self.lat:6f}, {self.lng:6f})" + base = f"({self.lat:6f}, {self.lng:6f})" + if self.horizontal_accuracy is not None or self.vertical_accuracy is not None: + accuracy = [] + if self.horizontal_accuracy is not None: + accuracy.append(f"horizontal_accuracy: {self.horizontal_accuracy:6f}m") + if self.vertical_accuracy is not None: + accuracy.append(f"vertical_accuracy: {self.vertical_accuracy:6f}m") + return f"{base} [{', '.join(accuracy)}]" + return base class Position(NamedTuple): From a447e855c68dd9a7e283fdc7e4266de4144f8cf2 Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 11:40:58 -0400 Subject: [PATCH 3/6] Added and fields to the Location class to represent measurement accuracies --- core/src/toga/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/types.py b/core/src/toga/types.py index c0c19d864b..c8993185c2 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -35,9 +35,9 @@ def __str__(self) -> str: if self.horizontal_accuracy is not None or self.vertical_accuracy is not None: accuracy = [] if self.horizontal_accuracy is not None: - accuracy.append(f"horizontal_accuracy: {self.horizontal_accuracy:6f}m") + accuracy.append(f"horizontal_accuracy: {self.horizontal_accuracy:6f}") if self.vertical_accuracy is not None: - accuracy.append(f"vertical_accuracy: {self.vertical_accuracy:6f}m") + accuracy.append(f"vertical_accuracy: {self.vertical_accuracy:6f}") return f"{base} [{', '.join(accuracy)}]" return base From 8abfe9bd25d9ace694dde431bf98f0f1595108f4 Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 11:42:10 -0400 Subject: [PATCH 4/6] Simplify accuracy field descriptions in Location type --- core/src/toga/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/toga/types.py b/core/src/toga/types.py index c8993185c2..81064f4fb0 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -24,10 +24,10 @@ class LatLng(NamedTuple): #: Longitude lng: float - #: Horizontal accuracy in meters + #: Horizontal accuracy horizontal_accuracy: float | None = None - #: Vertical accuracy in meters + #: Vertical accuracy vertical_accuracy: float | None = None def __str__(self) -> str: From fe3b4a9316f01bc269795058387d03a31d94c6e7 Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 12:42:50 -0400 Subject: [PATCH 5/6] Added support for horizontal_accuracy and vertical_accuracy in LatLng objects --- core/src/toga/types.py | 15 ++++++--------- core/tests/widgets/test_mapview.py | 28 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/core/src/toga/types.py b/core/src/toga/types.py index 81064f4fb0..761744a5b4 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -31,15 +31,12 @@ class LatLng(NamedTuple): vertical_accuracy: float | None = None def __str__(self) -> str: - base = f"({self.lat:6f}, {self.lng:6f})" - if self.horizontal_accuracy is not None or self.vertical_accuracy is not None: - accuracy = [] - if self.horizontal_accuracy is not None: - accuracy.append(f"horizontal_accuracy: {self.horizontal_accuracy:6f}") - if self.vertical_accuracy is not None: - accuracy.append(f"vertical_accuracy: {self.vertical_accuracy:6f}") - return f"{base} [{', '.join(accuracy)}]" - return base + values = [f"{self.lat:.6f}", f"{self.lng:.6f}"] + if self.horizontal_accuracy is not None: + values.append(f"{self.horizontal_accuracy:.6f}") + if self.vertical_accuracy is not None: + values.append(f"{self.vertical_accuracy:.6f}") + return f"({', '.join(values)})" class Position(NamedTuple): diff --git a/core/tests/widgets/test_mapview.py b/core/tests/widgets/test_mapview.py index f0cd464134..857ae571da 100644 --- a/core/tests/widgets/test_mapview.py +++ b/core/tests/widgets/test_mapview.py @@ -74,21 +74,25 @@ def test_create_with_values(pins): def test_latlng_properties(): """LatLng objects behave like tuples.""" # Create a LatLng object. - pos = toga.LatLng(37.42, 42.37123456) + pos = toga.LatLng(37.42, 42.37123456, 1.0, 2.0) # String representation is clean, and clipped to 6dp - assert str(pos) == "(37.420000, 42.371235)" + assert str(pos) == "(37.420000, 42.371235, 1.000000, 2.000000)" # Values can be accessed by attribute assert pos.lat == pytest.approx(37.42) assert pos.lng == pytest.approx(42.37123456) + assert pos.horizontal_accuracy == pytest.approx(1.0) + assert pos.vertical_accuracy == pytest.approx(2.0) # Values can be accessed by position assert pos[0] == pytest.approx(37.42) assert pos[1] == pytest.approx(42.37123456) + assert pos[2] == pytest.approx(1.0) + assert pos[3] == pytest.approx(2.0) # LatLng can be compared with tuples - assert pos == pytest.approx((37.42, 42.37123456)) + assert pos == pytest.approx((37.42, 42.37123456, 1.0, 2.0)) @pytest.mark.parametrize( @@ -114,21 +118,21 @@ def test_pin_location(widget): pin = toga.MapPin((37.42, 42.37), title="TheTitle", subtitle="TheSubtitle") assert isinstance(pin.location, toga.LatLng) - assert pin.location == (37.42, 42.37) + assert pin.location == (37.42, 42.37, None, None) # Change the pin location before the pin is on the map EventLog.reset() # Pin Location can be changed with a tuple - pin.location = (23.45, 67.89) + pin.location = (23.45, 67.89, 5.0, 6.0) assert isinstance(pin.location, toga.LatLng) - assert pin.location == (23.45, 67.89) + assert pin.location == (23.45, 67.89, 5.0, 6.0) assert_action_not_performed(widget, "update pin") # Pin Location can be changed with a LatLng - pin.location = toga.LatLng(12.34, 56.78) + pin.location = toga.LatLng(12.34, 56.78, 5.0, 6.0) assert isinstance(pin.location, toga.LatLng) - assert pin.location == (12.34, 56.78) + assert pin.location == (12.34, 56.78, 5.0, 6.0) assert_action_not_performed(widget, "update pin") # Add the pin to a map @@ -136,9 +140,9 @@ def test_pin_location(widget): assert_action_performed_with(widget, "add pin", pin=pin) # Pin Location can be changed while on the map - pin.location = (23.45, 67.89) + pin.location = (23.45, 67.89, None, None) assert isinstance(pin.location, toga.LatLng) - assert pin.location == (23.45, 67.89) + assert pin.location == (23.45, 67.89, None, None) assert_action_performed_with(widget, "update pin", pin=pin) # Remove the pin from the map @@ -147,9 +151,9 @@ def test_pin_location(widget): assert_action_performed_with(widget, "remove pin", pin=pin) # Updating the location doesn't modify the map - pin.location = toga.LatLng(12.34, 56.78) + pin.location = toga.LatLng(12.34, 56.78, 1.0, 2.0) assert isinstance(pin.location, toga.LatLng) - assert pin.location == (12.34, 56.78) + assert pin.location == (12.34, 56.78, 1.0, 2.0) assert_action_not_performed(widget, "update pin") From a47c83d2042e327ba30bb9da1dad0c16680f4210 Mon Sep 17 00:00:00 2001 From: brianchin Date: Tue, 20 May 2025 13:14:29 -0400 Subject: [PATCH 6/6] Add accuracy data to location results across platforms --- android/tests_backend/hardware/location.py | 2 ++ changes/3112.feature.rst | 1 + 2 files changed, 3 insertions(+) create mode 100644 changes/3112.feature.rst diff --git a/android/tests_backend/hardware/location.py b/android/tests_backend/hardware/location.py index 7675f25f3f..7596c29d8b 100644 --- a/android/tests_backend/hardware/location.py +++ b/android/tests_backend/hardware/location.py @@ -54,6 +54,8 @@ def add_location(self, location, altitude, cached=False): native_location = Location(LocationManager.FUSED_PROVIDER) native_location.setLatitude(location.lat) native_location.setLongitude(location.lng) + native_location.setAccuracy(10.0) # Set horizontal accuracy + native_location.setVerticalAccuracyMeters(5.0) if altitude: native_location.setAltitude(altitude) diff --git a/changes/3112.feature.rst b/changes/3112.feature.rst new file mode 100644 index 0000000000..ffcfc1d697 --- /dev/null +++ b/changes/3112.feature.rst @@ -0,0 +1 @@ +Added accuracy data to location results for Android, iOS, and macOS platforms.