-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathprovider.ts
124 lines (107 loc) · 3.79 KB
/
provider.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { PullProvider, ProviderInterrupter, ProviderInterruption } from '@awarns/core/providers';
import { Geolocation, GeolocationType } from './geolocation';
import {
GeolocationProvider as NativeProvider,
Geolocation as NativeGeolocation,
getGeolocationProvider as getNativeProvider,
} from 'nativescript-context-apis/geolocation';
import { firstValueFrom, from, Observable, of, Subject, throwError, timeout } from 'rxjs';
import { map, mergeMap, take, takeUntil, toArray } from 'rxjs/operators';
export class GeolocationProvider implements PullProvider {
get provides(): string {
return GeolocationType;
}
constructor(
private bestOf: number,
private timeout: number,
private nativeProvider: () => NativeProvider = getNativeProvider
) {}
async checkIfIsReady(): Promise<void> {
const isReady = await this.nativeProvider().isReady();
if (!isReady) {
throw geolocationProviderNotReadyErr;
}
}
prepare(): Promise<void> {
return this.nativeProvider().prepare(false, true);
}
next(): [Promise<Geolocation>, ProviderInterruption] {
const interrupter = new ProviderInterrupter();
const bestLocation = this.obtainBestLocationAmongNext(this.bestOf, interrupter);
return [bestLocation, () => interrupter.interrupt()];
}
private obtainBestLocationAmongNext(amount: number, interrupter: ProviderInterrupter): Promise<Geolocation> {
const interrupted = new Subject<void>();
interrupter.interruption = () => {
interrupted.next();
interrupted.complete();
};
return firstValueFrom(
this.nativeProvider()
.locationStream({
highAccuracy: true,
stdInterval: 1000,
minInterval: 100,
maxAge: 60000,
saveBattery: false,
})
.pipe(
takeUntil(interrupted),
take(amount),
timeout({ each: this.timeout, with: () => of(null) }),
toArray(),
map(pickBest),
mergeMap((location) => this.ensureItGetsAtLeastOne(location)),
map(toGeolocation)
)
);
}
private ensureItGetsAtLeastOne(location: NativeGeolocation): Observable<NativeGeolocation> {
if (!location) {
return from(
this.nativeProvider().acquireLocation({
highAccuracy: true,
allowBackground: true,
})
).pipe(
timeout({
each: this.timeout,
with: () => throwError(() => new Error('Could not acquire location')),
})
);
}
return of(location);
}
}
export const geolocationProviderNotReadyErr = new Error(
"Geolocation provider is not ready. Perhaps permissions haven't been granted or location services have been disabled"
);
function pickBest(locations: Array<NativeGeolocation>): NativeGeolocation {
const now = Date.now();
return locations.reduce(
(previous, current) =>
current && (!previous || calculateScore(current, now) > calculateScore(previous, now)) ? current : previous,
null
);
}
function calculateScore(location: NativeGeolocation, now: number): number {
const { horizontalAccuracy, timestamp } = location;
const timeDiff = (now - timestamp.getTime()) / 1000;
const limitedAccuracy = Math.min(horizontalAccuracy, 65);
const limitedTimeDiff = Math.min(Math.max(timeDiff, 0), 60);
const accuracyScore = 1 - limitedAccuracy / 65;
const timeDiffScore = 1 - limitedTimeDiff / 60;
return ((accuracyScore + timeDiffScore) / 2) * 10;
}
function toGeolocation(nativeGeolocation: NativeGeolocation): Geolocation {
return new Geolocation(
nativeGeolocation.latitude,
nativeGeolocation.longitude,
nativeGeolocation.altitude,
nativeGeolocation.horizontalAccuracy,
nativeGeolocation.verticalAccuracy,
nativeGeolocation.speed,
nativeGeolocation.direction,
nativeGeolocation.timestamp
);
}