Add initial core API and docs for geolocation.
This commit is contained in:
parent
a136ec2549
commit
e20010d8af
7 changed files with 214 additions and 5 deletions
|
|
@ -24,6 +24,7 @@ from toga.command import Command, CommandSet
|
|||
from toga.documents import Document
|
||||
from toga.handlers import wrapped_handler
|
||||
from toga.hardware.camera import Camera
|
||||
from toga.hardware.geolocation import Geolocation
|
||||
from toga.icons import Icon
|
||||
from toga.paths import Paths
|
||||
from toga.platform import get_platform_factory
|
||||
|
|
@ -670,6 +671,18 @@ class App:
|
|||
"""The commands available in the app."""
|
||||
return self._commands
|
||||
|
||||
@property
|
||||
def geolocation(self) -> Geolocation:
|
||||
"""A representation of the device's geolocation service."""
|
||||
try:
|
||||
return self._geolocation
|
||||
except AttributeError:
|
||||
# Instantiate the geolocation service for this app on first access
|
||||
# This will raise an exception if the platform doesn't implement
|
||||
# the Geolocation API.
|
||||
self._geolocation = Geolocation(self)
|
||||
return self._geolocation
|
||||
|
||||
@property
|
||||
def paths(self) -> Paths:
|
||||
"""Paths for platform-appropriate locations on the user's file system.
|
||||
|
|
|
|||
|
|
@ -163,3 +163,7 @@ class AsyncResult(ABC):
|
|||
__ne__ = __bool__
|
||||
__gt__ = __bool__
|
||||
__ge__ = __bool__
|
||||
|
||||
|
||||
class PermissionResult(AsyncResult):
|
||||
RESULT_TYPE = "permission"
|
||||
|
|
|
|||
|
|
@ -3,17 +3,13 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from toga.constants import FlashMode
|
||||
from toga.handlers import AsyncResult
|
||||
from toga.handlers import AsyncResult, PermissionResult
|
||||
from toga.platform import get_platform_factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from toga.app import App
|
||||
|
||||
|
||||
class PermissionResult(AsyncResult):
|
||||
RESULT_TYPE = "permission"
|
||||
|
||||
|
||||
class PhotoResult(AsyncResult):
|
||||
RESULT_TYPE = "photo"
|
||||
|
||||
|
|
|
|||
120
core/src/toga/hardware/geolocation.py
Normal file
120
core/src/toga/hardware/geolocation.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
import toga
|
||||
from toga.handlers import AsyncResult, PermissionResult, wrapped_handler
|
||||
from toga.platform import get_platform_factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from toga.app import App
|
||||
|
||||
|
||||
class LocationResult(AsyncResult):
|
||||
RESULT_TYPE = "location"
|
||||
|
||||
|
||||
class OnLocationChangeHandler(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
geolocation: Geolocation,
|
||||
location: toga.LatLng,
|
||||
altitude: float | None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""A handler that will be invoked when the user's location changes.
|
||||
|
||||
:param geolocation: the Geolocation service that generated the update.
|
||||
:param location: The user's location as (latitude, longitude).
|
||||
:param altitude: The user's altitude in meters above WGS84 reference ellipsoid.
|
||||
Returns None if the altitude could not be determined.
|
||||
:param kwargs: Ensures compatibility with arguments added in future versions.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class Geolocation:
|
||||
def __init__(self, app: App):
|
||||
self.factory = get_platform_factory()
|
||||
self._app = app
|
||||
self._impl = self.factory.Geolocation(self)
|
||||
|
||||
self.on_change = None
|
||||
|
||||
@property
|
||||
def app(self) -> App:
|
||||
"""The app with which the geolocation service is associated"""
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def has_permission(self) -> bool:
|
||||
"""Does the app have permission to use geolocation services?
|
||||
|
||||
If the platform requires the user to explicitly confirm permission, and
|
||||
the user has not yet given permission, this will return ``False``.
|
||||
"""
|
||||
return self._impl.has_permission()
|
||||
|
||||
def request_permission(self) -> PermissionResult:
|
||||
"""Request sufficient permissions to capture the user's location.
|
||||
|
||||
If permission has already been granted, this will return without prompting the
|
||||
user.
|
||||
|
||||
**This is an asynchronous method**. If you invoke this method in synchronous
|
||||
context, it will start the process of requesting permissions, but will return
|
||||
*immediately*. The return value can be awaited in an asynchronous context, but
|
||||
cannot be compared directly.
|
||||
|
||||
:returns: An asynchronous result; when awaited, returns True if the app has
|
||||
permission to capture the user's a geolocation; False otherwise.
|
||||
"""
|
||||
result = PermissionResult(None)
|
||||
|
||||
if has_permission := self.has_permission:
|
||||
result.set_result(has_permission)
|
||||
else:
|
||||
self._impl.request_permission(result)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def on_change(self) -> OnLocationChangeHandler:
|
||||
"""The handler to invoke when the user's location changes."""
|
||||
return self._on_change
|
||||
|
||||
@on_change.setter
|
||||
def on_change(self, handler):
|
||||
self._on_change = wrapped_handler(self, handler)
|
||||
|
||||
def start(self):
|
||||
"""Start monitoring the user's location for changes.
|
||||
|
||||
An :any:`on_change` callback will be generated then when the user's location
|
||||
changes.
|
||||
"""
|
||||
self._impl.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop monitoring the user's location."""
|
||||
self._impl.stop()
|
||||
|
||||
@property
|
||||
def current_location(self) -> LocationResult:
|
||||
"""Obtain the user's current location using the geolocation service.
|
||||
|
||||
If the platform requires permission to access the geolocation service, and the
|
||||
user hasn't previously provided that permission, this will cause permission to
|
||||
be requested.
|
||||
|
||||
**This is an asynchronous property**. If you request this property in
|
||||
synchronous context, it will start the process of requesting the user's
|
||||
location, but will return *immediately*. The return value can be awaited in an
|
||||
asynchronous context, but cannot be compared directly.
|
||||
|
||||
:returns: An asynchronous result; when awaited, returns the :any:`toga.Image`
|
||||
captured by the camera, or ``None`` if the photo was cancelled.
|
||||
"""
|
||||
location = LocationResult(None)
|
||||
self._impl.current_location(location)
|
||||
return location
|
||||
74
docs/reference/api/hardware/geolocation.rst
Normal file
74
docs/reference/api/hardware/geolocation.rst
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
Geolocation
|
||||
===========
|
||||
|
||||
A sensor that can capture the geographical location of the device.
|
||||
|
||||
.. rst-class:: widget-support
|
||||
.. csv-filter:: Availability (:ref:`Key <api-status-key>`)
|
||||
:header-rows: 1
|
||||
:file: ../../data/widgets_by_platform.csv
|
||||
:included_cols: 4,5,6,7,8,9,10
|
||||
:exclude: {0: '(?!(Geolocation|Hardware))'}
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
The Geolocation services of a device can be accessed using the
|
||||
:attr:`~toga.App.geolocation` attribute. This attribute exposes an API that allows you to
|
||||
check if you have have permission to access geolocation services; and if permission exists,
|
||||
capture the current location of the device, and set a handler to be notified when position
|
||||
changes occur.
|
||||
|
||||
The Camera API is *asynchronous*. This means the methods that have long-running behavior
|
||||
(such as requesting permissions and requesting a position) must be ``await``-ed, rather
|
||||
than being invoked directly. This means they must be invoked from inside an asynchronous
|
||||
handler:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import toga
|
||||
|
||||
class MyApp(toga.App):
|
||||
...
|
||||
async def determine_location(self, widget, **kwargs):
|
||||
location = await self.geolocation.current_location
|
||||
|
||||
Most platforms will require some form of device permission to access the geolocation
|
||||
service. To confirm if you have permission to use the geolocation service, you can call
|
||||
:any:`Geolocation.has_permission`; you can request to permission using
|
||||
:any:`Geolocation.request_permission()`.
|
||||
|
||||
The calls to request permissions *can* be invoked from a synchronous context (i.e., a
|
||||
non ``async`` method); however, they are non-blocking when used in this way. Invoking a
|
||||
method like :any:`Geolocation.request_permission()` will start the process of requesting
|
||||
permission, but will return *immediately*, without waiting for the user's response. This
|
||||
allows an app to *request* permissions as part of the startup process, prior to using
|
||||
the geolocation APIs, without blocking the rest of app startup.
|
||||
|
||||
Toga will confirm whether the app has been granted permission to use geolocation
|
||||
services before invoking any geolocation API. If permission has not yet been granted,
|
||||
the platform *may* request access at the time of the first geolocation request; however,
|
||||
this is not guaranteed to be the behavior on all platforms.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
* Apps that use a camera must be configured to provide permission to the camera device.
|
||||
The permissions required are platform specific:
|
||||
|
||||
* iOS: ``NSLocationWhenInUseUsageDescription`` must be defined in the app's
|
||||
``Info.plist`` file. If you want to track location while the app is in the
|
||||
background, you must also define ``NSLocationAlwaysAndWhenInUseUsageDescription``.
|
||||
* macOS: The ``com.apple.security.personal-information.location`` entitlement must be
|
||||
enabled, and ``NSLocationUsageDescription`` must be defined in the app's
|
||||
``Info.plist`` file.
|
||||
* Android: At least one of the permissions ``android.permission.ACCESS_FINE_LOCATION``
|
||||
or ``android.permission.ACCESS_COARSE_LOCATION`` must be declared; if only one is
|
||||
declared, this will impact on the precision available in geolocation results. If you
|
||||
want to track location while the app is in the background, you must also define the
|
||||
permission ``android.permission.ACCESS_BACKGROUND_LOCATION``.
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
.. autoclass:: toga.hardware.geolocation.Geolocation
|
||||
|
|
@ -4,3 +4,4 @@ Device and Hardware
|
|||
.. toctree::
|
||||
|
||||
camera
|
||||
geolocation
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that ca
|
|||
SplitContainer,Layout Widget,:class:`~toga.SplitContainer`,A container that divides an area into two panels with a movable border,|y|,|y|,|y|,,,,
|
||||
OptionContainer,Layout Widget,:class:`~toga.OptionContainer`,A container that can display multiple labeled tabs of content,|y|,|y|,|y|,|y|,|y|,,
|
||||
Camera,Hardware,:class:`~toga.hardware.camera.Camera`,A sensor that can capture photos and/or video.,|y|,,,|y|,|y|,,
|
||||
Geolocation,Hardware,:class:`~toga.hardware.geolocation.Geolocation`,A sensor that can capture the geographical location of the device.,|y|,,,|y|,|y|,,
|
||||
Screen,Hardware,:class:`~toga.screens.Screen`,A representation of a screen attached to a device.,|y|,|y|,|y|,|y|,|y|,|b|,|b|
|
||||
App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b|
|
||||
Command,Resource,:class:`~toga.Command`,Command,|y|,|y|,|y|,,|y|,,
|
||||
|
|
|
|||
|
Loading…
Reference in a new issue