@tokamakdev/plugin-location 0.1.0-beta.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/android/LocationPlugin.kt +180 -0
- package/apple/LocationPlugin.swift +174 -0
- package/package.json +27 -0
- package/src/index.ts +44 -0
- package/tokamak-plugin.json +39 -0
- package/web/index.ts +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @tokamakdev/plugin-location
|
|
2
|
+
|
|
3
|
+
Location for Tokamak applications and the web.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { location } from "@tokamakdev/plugin-location";
|
|
7
|
+
|
|
8
|
+
const position = await location.getCurrentPosition();
|
|
9
|
+
|
|
10
|
+
const stop = location.watchPosition(
|
|
11
|
+
(next) => console.log(next.coords.latitude, next.coords.longitude),
|
|
12
|
+
console.error,
|
|
13
|
+
);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The web implementation uses `navigator.geolocation`. Native Tokamak builds use
|
|
17
|
+
Core Location on Apple platforms, `LocationManager` on Android, and WebView2's
|
|
18
|
+
location implementation on Windows. Calling the API on a platform without
|
|
19
|
+
location support throws `NotSupportedError`.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package com.tokamak.plugins.location
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.location.Location
|
|
8
|
+
import android.location.LocationListener
|
|
9
|
+
import android.location.LocationManager
|
|
10
|
+
import android.os.Bundle
|
|
11
|
+
import android.os.CancellationSignal
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import com.tokamak.runtime.TokamakPlugin
|
|
14
|
+
import com.tokamak.runtime.TokamakPluginError
|
|
15
|
+
import com.tokamak.runtime.TokamakPluginReply
|
|
16
|
+
|
|
17
|
+
internal class TokamakLocationPlugin(
|
|
18
|
+
private val activity: Activity,
|
|
19
|
+
) : TokamakPlugin {
|
|
20
|
+
override val id = "location"
|
|
21
|
+
|
|
22
|
+
private val manager =
|
|
23
|
+
activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
24
|
+
private val permissionRequests = mutableListOf<(Boolean) -> Unit>()
|
|
25
|
+
|
|
26
|
+
override fun call(method: String, arguments: Any?, reply: TokamakPluginReply) {
|
|
27
|
+
if (method != "getCurrentPosition") {
|
|
28
|
+
super.call(method, arguments, reply)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
withPermission { granted ->
|
|
32
|
+
if (granted) currentPosition(reply) else reply(permissionDenied())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun subscribe(
|
|
37
|
+
method: String,
|
|
38
|
+
arguments: Any?,
|
|
39
|
+
reply: TokamakPluginReply,
|
|
40
|
+
): () -> Unit {
|
|
41
|
+
if (method != "watchPosition") return super.subscribe(method, arguments, reply)
|
|
42
|
+
var cancelled = false
|
|
43
|
+
var stop = {}
|
|
44
|
+
withPermission { granted ->
|
|
45
|
+
if (!granted) {
|
|
46
|
+
reply(permissionDenied())
|
|
47
|
+
} else if (!cancelled) {
|
|
48
|
+
stop = watchPosition(reply)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
cancelled = true
|
|
53
|
+
stop()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onRequestPermissionsResult(
|
|
58
|
+
requestCode: Int,
|
|
59
|
+
permissions: Array<out String>,
|
|
60
|
+
grantResults: IntArray,
|
|
61
|
+
) {
|
|
62
|
+
if (requestCode != LOCATION_PERMISSION_REQUEST) return
|
|
63
|
+
val granted = grantResults.any { it == PackageManager.PERMISSION_GRANTED }
|
|
64
|
+
val requests = permissionRequests.toList()
|
|
65
|
+
permissionRequests.clear()
|
|
66
|
+
requests.forEach { it(granted) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun withPermission(action: (Boolean) -> Unit) {
|
|
70
|
+
if (hasPermission()) {
|
|
71
|
+
action(true)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
permissionRequests.add(action)
|
|
75
|
+
if (permissionRequests.size > 1) return
|
|
76
|
+
activity.requestPermissions(
|
|
77
|
+
arrayOf(
|
|
78
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
79
|
+
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
80
|
+
),
|
|
81
|
+
LOCATION_PERMISSION_REQUEST,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private fun hasPermission(): Boolean =
|
|
86
|
+
activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
|
|
87
|
+
PackageManager.PERMISSION_GRANTED ||
|
|
88
|
+
activity.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
|
|
89
|
+
PackageManager.PERMISSION_GRANTED
|
|
90
|
+
|
|
91
|
+
private fun currentPosition(reply: TokamakPluginReply) {
|
|
92
|
+
val provider = runCatching(::provider).getOrElse {
|
|
93
|
+
reply(locationFailure(it, "Location is unavailable"))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (provider == null) {
|
|
97
|
+
reply(unavailable("No location provider is available"))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
runCatching {
|
|
101
|
+
manager.getCurrentLocation(
|
|
102
|
+
provider,
|
|
103
|
+
CancellationSignal(),
|
|
104
|
+
activity.mainExecutor,
|
|
105
|
+
) { location ->
|
|
106
|
+
if (location == null) reply(unavailable("Location is unavailable"))
|
|
107
|
+
else reply(Result.success(position(location)))
|
|
108
|
+
}
|
|
109
|
+
}.onFailure { reply(locationFailure(it, "Location is unavailable")) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun watchPosition(reply: TokamakPluginReply): () -> Unit {
|
|
113
|
+
val provider = runCatching(::provider).getOrElse {
|
|
114
|
+
reply(locationFailure(it, "Location is unavailable"))
|
|
115
|
+
return {}
|
|
116
|
+
}
|
|
117
|
+
if (provider == null) {
|
|
118
|
+
reply(unavailable("No location provider is available"))
|
|
119
|
+
return {}
|
|
120
|
+
}
|
|
121
|
+
val listener = object : LocationListener {
|
|
122
|
+
override fun onLocationChanged(location: Location) {
|
|
123
|
+
reply(Result.success(position(location)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onProviderDisabled(provider: String) {
|
|
127
|
+
reply(unavailable("Location provider is unavailable"))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
|
131
|
+
}
|
|
132
|
+
val started = runCatching {
|
|
133
|
+
manager.requestLocationUpdates(provider, 1000, 0F, listener, Looper.getMainLooper())
|
|
134
|
+
}
|
|
135
|
+
if (started.isFailure) {
|
|
136
|
+
reply(locationFailure(started.exceptionOrNull(), "Location is unavailable"))
|
|
137
|
+
return {}
|
|
138
|
+
}
|
|
139
|
+
return { manager.removeUpdates(listener) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun provider(): String? =
|
|
143
|
+
listOf(
|
|
144
|
+
LocationManager.GPS_PROVIDER,
|
|
145
|
+
LocationManager.NETWORK_PROVIDER,
|
|
146
|
+
LocationManager.FUSED_PROVIDER,
|
|
147
|
+
).firstOrNull(manager::isProviderEnabled)
|
|
148
|
+
?: manager.getProviders(true).firstOrNull()
|
|
149
|
+
|
|
150
|
+
private fun position(location: Location): Map<String, Any?> =
|
|
151
|
+
mapOf(
|
|
152
|
+
"coords" to
|
|
153
|
+
mapOf(
|
|
154
|
+
"latitude" to location.latitude,
|
|
155
|
+
"longitude" to location.longitude,
|
|
156
|
+
"accuracy" to location.accuracy.toDouble(),
|
|
157
|
+
"altitude" to location.altitude.takeIf { location.hasAltitude() },
|
|
158
|
+
"altitudeAccuracy" to
|
|
159
|
+
location.verticalAccuracyMeters.toDouble()
|
|
160
|
+
.takeIf { location.hasVerticalAccuracy() },
|
|
161
|
+
"heading" to location.bearing.toDouble().takeIf { location.hasBearing() },
|
|
162
|
+
"speed" to location.speed.toDouble().takeIf { location.hasSpeed() },
|
|
163
|
+
),
|
|
164
|
+
"timestamp" to location.time,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
private fun permissionDenied(): Result<Any?> =
|
|
168
|
+
Result.failure(TokamakPluginError("NotAllowedError", "Location permission was denied"))
|
|
169
|
+
|
|
170
|
+
private fun unavailable(message: String): Result<Any?> =
|
|
171
|
+
Result.failure(TokamakPluginError("NotReadableError", message))
|
|
172
|
+
|
|
173
|
+
private fun locationFailure(error: Throwable?, fallback: String): Result<Any?> =
|
|
174
|
+
if (error is SecurityException) permissionDenied()
|
|
175
|
+
else unavailable(error?.message ?: fallback)
|
|
176
|
+
|
|
177
|
+
private companion object {
|
|
178
|
+
const val LOCATION_PERMISSION_REQUEST = 0xA771
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
5
|
+
CLLocationManagerDelegate
|
|
6
|
+
{
|
|
7
|
+
let id = "location"
|
|
8
|
+
|
|
9
|
+
private let manager = CLLocationManager()
|
|
10
|
+
private var current: [TokamakPluginReply] = []
|
|
11
|
+
private var watchers: [UUID: TokamakPluginReply] = [:]
|
|
12
|
+
|
|
13
|
+
override init() {
|
|
14
|
+
super.init()
|
|
15
|
+
manager.delegate = self
|
|
16
|
+
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func call(
|
|
20
|
+
method: String,
|
|
21
|
+
arguments: Any,
|
|
22
|
+
reply: @escaping TokamakPluginReply
|
|
23
|
+
) {
|
|
24
|
+
guard method == "getCurrentPosition" else {
|
|
25
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
current.append(reply)
|
|
29
|
+
start()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func subscribe(
|
|
33
|
+
method: String,
|
|
34
|
+
arguments: Any,
|
|
35
|
+
reply: @escaping TokamakPluginReply
|
|
36
|
+
) -> (() -> Void) {
|
|
37
|
+
guard method == "watchPosition" else {
|
|
38
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
41
|
+
let subscription = UUID()
|
|
42
|
+
watchers[subscription] = reply
|
|
43
|
+
start()
|
|
44
|
+
return { [weak self] in
|
|
45
|
+
self?.watchers.removeValue(forKey: subscription)
|
|
46
|
+
self?.stopIfIdle()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func locationManagerDidChangeAuthorization(
|
|
51
|
+
_ manager: CLLocationManager
|
|
52
|
+
) {
|
|
53
|
+
switch manager.authorizationStatus {
|
|
54
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
55
|
+
manager.startUpdatingLocation()
|
|
56
|
+
case .denied, .restricted:
|
|
57
|
+
fail(
|
|
58
|
+
TokamakPluginError(
|
|
59
|
+
name: "NotAllowedError",
|
|
60
|
+
message: "Location permission was denied"
|
|
61
|
+
))
|
|
62
|
+
case .notDetermined:
|
|
63
|
+
break
|
|
64
|
+
@unknown default:
|
|
65
|
+
fail(.notSupported("Location authorization is not supported"))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func locationManager(
|
|
70
|
+
_ manager: CLLocationManager,
|
|
71
|
+
didUpdateLocations locations: [CLLocation]
|
|
72
|
+
) {
|
|
73
|
+
guard let location = locations.last else { return }
|
|
74
|
+
let result = Result<Any?, TokamakPluginError>.success(position(location))
|
|
75
|
+
let waiting = current
|
|
76
|
+
current.removeAll()
|
|
77
|
+
for reply in waiting {
|
|
78
|
+
reply(result)
|
|
79
|
+
}
|
|
80
|
+
for reply in watchers.values {
|
|
81
|
+
reply(result)
|
|
82
|
+
}
|
|
83
|
+
stopIfIdle()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func locationManager(
|
|
87
|
+
_ manager: CLLocationManager,
|
|
88
|
+
didFailWithError error: Error
|
|
89
|
+
) {
|
|
90
|
+
if (error as NSError).code == CLError.Code.locationUnknown.rawValue {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
fail(locationError(error))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func start() {
|
|
97
|
+
guard CLLocationManager.locationServicesEnabled() else {
|
|
98
|
+
fail(unavailable("Location services are disabled"))
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
switch manager.authorizationStatus {
|
|
102
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
103
|
+
manager.startUpdatingLocation()
|
|
104
|
+
case .notDetermined:
|
|
105
|
+
manager.requestWhenInUseAuthorization()
|
|
106
|
+
case .denied, .restricted:
|
|
107
|
+
fail(
|
|
108
|
+
TokamakPluginError(
|
|
109
|
+
name: "NotAllowedError",
|
|
110
|
+
message: "Location permission was denied"
|
|
111
|
+
))
|
|
112
|
+
@unknown default:
|
|
113
|
+
fail(.notSupported("Location authorization is not supported"))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func fail(_ error: TokamakPluginError) {
|
|
118
|
+
let result = Result<Any?, TokamakPluginError>.failure(error)
|
|
119
|
+
let replies = current + Array(watchers.values)
|
|
120
|
+
current.removeAll()
|
|
121
|
+
for reply in replies {
|
|
122
|
+
reply(result)
|
|
123
|
+
}
|
|
124
|
+
if watchers.isEmpty {
|
|
125
|
+
manager.stopUpdatingLocation()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func locationError(_ error: Error) -> TokamakPluginError {
|
|
130
|
+
if (error as NSError).code == CLError.Code.denied.rawValue {
|
|
131
|
+
return TokamakPluginError(
|
|
132
|
+
name: "NotAllowedError",
|
|
133
|
+
message: "Location permission was denied"
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return unavailable(error.localizedDescription)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func unavailable(_ message: String) -> TokamakPluginError {
|
|
140
|
+
TokamakPluginError(name: "NotReadableError", message: message)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func stopIfIdle() {
|
|
144
|
+
if current.isEmpty && watchers.isEmpty {
|
|
145
|
+
manager.stopUpdatingLocation()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func position(_ location: CLLocation) -> [String: Any] {
|
|
150
|
+
let coordinates = location.coordinate
|
|
151
|
+
return [
|
|
152
|
+
"coords": [
|
|
153
|
+
"latitude": coordinates.latitude,
|
|
154
|
+
"longitude": coordinates.longitude,
|
|
155
|
+
"accuracy": location.horizontalAccuracy,
|
|
156
|
+
"altitude": nullable(
|
|
157
|
+
location.verticalAccuracy >= 0,
|
|
158
|
+
location.altitude
|
|
159
|
+
),
|
|
160
|
+
"altitudeAccuracy": nullable(
|
|
161
|
+
location.verticalAccuracy >= 0,
|
|
162
|
+
location.verticalAccuracy
|
|
163
|
+
),
|
|
164
|
+
"heading": nullable(location.course >= 0, location.course),
|
|
165
|
+
"speed": nullable(location.speed >= 0, location.speed),
|
|
166
|
+
],
|
|
167
|
+
"timestamp": location.timestamp.timeIntervalSince1970 * 1000,
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func nullable(_ available: Bool, _ value: Double) -> Any {
|
|
172
|
+
available ? value : NSNull()
|
|
173
|
+
}
|
|
174
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokamakdev/plugin-location",
|
|
3
|
+
"version": "0.1.0-beta.33",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/mantty/tokamak.git",
|
|
7
|
+
"directory": "plugins/location"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@tokamakdev/plugin": "0.1.0-beta.33"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"android",
|
|
18
|
+
"apple",
|
|
19
|
+
"src",
|
|
20
|
+
"web",
|
|
21
|
+
"tokamak-plugin.json",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FrontendPlugin } from "@tokamakdev/plugin";
|
|
2
|
+
import * as web from "../web/index.js";
|
|
3
|
+
|
|
4
|
+
export interface Coordinates {
|
|
5
|
+
readonly latitude: number;
|
|
6
|
+
readonly longitude: number;
|
|
7
|
+
readonly accuracy: number;
|
|
8
|
+
readonly altitude: number | null;
|
|
9
|
+
readonly altitudeAccuracy: number | null;
|
|
10
|
+
readonly heading: number | null;
|
|
11
|
+
readonly speed: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Position {
|
|
15
|
+
readonly coords: Coordinates;
|
|
16
|
+
readonly timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PositionCallback = (position: Position) => void;
|
|
20
|
+
export type PositionErrorCallback = (error: DOMException) => void;
|
|
21
|
+
|
|
22
|
+
class Location extends FrontendPlugin {
|
|
23
|
+
constructor() {
|
|
24
|
+
super("location");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getCurrentPosition(): Promise<Position> {
|
|
28
|
+
if (this.hasNativeTransport) {
|
|
29
|
+
return this.call("getCurrentPosition");
|
|
30
|
+
}
|
|
31
|
+
return web.getCurrentPosition();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
watchPosition(next: PositionCallback, error: PositionErrorCallback): () => void {
|
|
35
|
+
if (this.hasNativeTransport) {
|
|
36
|
+
return this.subscribe("watchPosition", (position) => {
|
|
37
|
+
next(position as Position);
|
|
38
|
+
}, error);
|
|
39
|
+
}
|
|
40
|
+
return web.watchPosition(next, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const location = new Location();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "location",
|
|
4
|
+
"kind": "frontend",
|
|
5
|
+
"platforms": {
|
|
6
|
+
"macos": {
|
|
7
|
+
"class": "TokamakLocationPlugin",
|
|
8
|
+
"sources": ["apple/LocationPlugin.swift"],
|
|
9
|
+
"frameworks": ["CoreLocation"],
|
|
10
|
+
"plist": {
|
|
11
|
+
"NSLocationUsageDescription": "This app uses your location when you request location features."
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"ios": {
|
|
15
|
+
"class": "TokamakLocationPlugin",
|
|
16
|
+
"sources": ["apple/LocationPlugin.swift"],
|
|
17
|
+
"frameworks": ["CoreLocation"],
|
|
18
|
+
"plist": {
|
|
19
|
+
"NSLocationWhenInUseUsageDescription": "This app uses your location when you request location features."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"ios-simulator": {
|
|
23
|
+
"class": "TokamakLocationPlugin",
|
|
24
|
+
"sources": ["apple/LocationPlugin.swift"],
|
|
25
|
+
"frameworks": ["CoreLocation"],
|
|
26
|
+
"plist": {
|
|
27
|
+
"NSLocationWhenInUseUsageDescription": "This app uses your location when you request location features."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"android": {
|
|
31
|
+
"class": "com.tokamak.plugins.location.TokamakLocationPlugin",
|
|
32
|
+
"sources": ["android/LocationPlugin.kt"],
|
|
33
|
+
"permissions": [
|
|
34
|
+
"android.permission.ACCESS_COARSE_LOCATION",
|
|
35
|
+
"android.permission.ACCESS_FINE_LOCATION"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/web/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Position, PositionCallback, PositionErrorCallback } from "../src/index.js";
|
|
2
|
+
|
|
3
|
+
export function getCurrentPosition(): Promise<Position> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const browserLocation = browserLocationApi();
|
|
6
|
+
browserLocation.getCurrentPosition(
|
|
7
|
+
(position) => {
|
|
8
|
+
resolve(copyPosition(position));
|
|
9
|
+
},
|
|
10
|
+
(error) => {
|
|
11
|
+
reject(positionError(error));
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function watchPosition(
|
|
18
|
+
next: PositionCallback,
|
|
19
|
+
error: PositionErrorCallback,
|
|
20
|
+
): () => void {
|
|
21
|
+
const browserLocation = browserLocationApi();
|
|
22
|
+
const id = browserLocation.watchPosition(
|
|
23
|
+
(position) => {
|
|
24
|
+
next(copyPosition(position));
|
|
25
|
+
},
|
|
26
|
+
(failure) => {
|
|
27
|
+
error(positionError(failure));
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
return () => {
|
|
31
|
+
browserLocation.clearWatch(id);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function browserLocationApi(): Geolocation {
|
|
36
|
+
const root: { navigator?: { geolocation?: Geolocation } } = globalThis;
|
|
37
|
+
const browserLocation = root.navigator?.geolocation;
|
|
38
|
+
if (browserLocation) return browserLocation;
|
|
39
|
+
throw new DOMException("Location is unavailable", "NotSupportedError");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function copyPosition(position: GeolocationPosition): Position {
|
|
43
|
+
const { coords } = position;
|
|
44
|
+
return {
|
|
45
|
+
coords: {
|
|
46
|
+
latitude: coords.latitude,
|
|
47
|
+
longitude: coords.longitude,
|
|
48
|
+
accuracy: coords.accuracy,
|
|
49
|
+
altitude: coords.altitude,
|
|
50
|
+
altitudeAccuracy: coords.altitudeAccuracy,
|
|
51
|
+
heading: coords.heading,
|
|
52
|
+
speed: coords.speed,
|
|
53
|
+
},
|
|
54
|
+
timestamp: position.timestamp,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function positionError(error: GeolocationPositionError): DOMException {
|
|
59
|
+
const name = {
|
|
60
|
+
[error.PERMISSION_DENIED]: "NotAllowedError",
|
|
61
|
+
[error.POSITION_UNAVAILABLE]: "NotReadableError",
|
|
62
|
+
[error.TIMEOUT]: "TimeoutError",
|
|
63
|
+
}[error.code];
|
|
64
|
+
return new DOMException(error.message, name ?? "UnknownError");
|
|
65
|
+
}
|