@umituz/react-native-location 1.0.6 → 1.0.8
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/package.json +1 -1
- package/src/domain/entities/Location.ts +1 -23
- package/src/index.ts +4 -0
- package/src/infrastructure/services/LocationService.ts +179 -31
- package/src/infrastructure/services/LocationWatcher.ts +116 -0
- package/src/infrastructure/utils/LocationUtils.ts +83 -0
- package/src/presentation/hooks/useLocation.ts +19 -9
- package/src/presentation/hooks/useLocationWatch.ts +75 -0
- package/src/types/location.types.ts +78 -0
package/package.json
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
latitude: number;
|
|
3
|
-
longitude: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface LocationAddress {
|
|
7
|
-
city?: string | null;
|
|
8
|
-
region?: string | null;
|
|
9
|
-
country?: string | null;
|
|
10
|
-
street?: string | null;
|
|
11
|
-
formattedAddress?: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface LocationData {
|
|
15
|
-
coords: Coordinates;
|
|
16
|
-
timestamp: number;
|
|
17
|
-
address?: LocationAddress;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface LocationError {
|
|
21
|
-
code: string;
|
|
22
|
-
message: string;
|
|
23
|
-
}
|
|
1
|
+
export * from "../../types/location.types";
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from "./domain/entities/Location";
|
|
2
|
+
export * from "./types/location.types";
|
|
2
3
|
export * from "./infrastructure/services/LocationService";
|
|
4
|
+
export * from "./infrastructure/services/LocationWatcher";
|
|
5
|
+
export * from "./infrastructure/utils/LocationUtils";
|
|
3
6
|
export * from "./presentation/hooks/useLocation";
|
|
7
|
+
export * from "./presentation/hooks/useLocationWatch";
|
|
@@ -1,53 +1,140 @@
|
|
|
1
1
|
import * as Location from "expo-location";
|
|
2
|
-
import {
|
|
2
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
3
|
+
import {
|
|
4
|
+
LocationData,
|
|
5
|
+
LocationConfig,
|
|
6
|
+
DEFAULT_LOCATION_CONFIG,
|
|
7
|
+
CachedLocationData,
|
|
8
|
+
LocationErrorImpl,
|
|
9
|
+
LocationErrorCode,
|
|
10
|
+
} from "../../types/location.types";
|
|
11
|
+
|
|
12
|
+
declare const __DEV__: boolean;
|
|
3
13
|
|
|
4
14
|
export class LocationService {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
private config: LocationConfig;
|
|
16
|
+
private storage = storageRepository;
|
|
17
|
+
|
|
18
|
+
constructor(config: LocationConfig = {}) {
|
|
19
|
+
this.config = { ...DEFAULT_LOCATION_CONFIG, ...config };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private log(message: string, ...args: unknown[]): void {
|
|
23
|
+
if (__DEV__) {
|
|
24
|
+
console.log(`[LocationService] ${message}`, ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private logError(message: string, error: unknown): void {
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.error(`[LocationService] ${message}`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private logWarn(message: string, ...args: unknown[]): void {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn(`[LocationService] ${message}`, ...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
async requestPermissions(): Promise<boolean> {
|
|
10
41
|
try {
|
|
11
|
-
|
|
42
|
+
this.log("Requesting permissions...");
|
|
12
43
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
13
|
-
|
|
44
|
+
this.log("Permission status:", status);
|
|
14
45
|
return status === "granted";
|
|
15
46
|
} catch (error) {
|
|
16
|
-
|
|
47
|
+
this.logError("Error requesting permissions:", error);
|
|
17
48
|
return false;
|
|
18
49
|
}
|
|
19
50
|
}
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
private async getCachedLocation(): Promise<LocationData | null> {
|
|
53
|
+
if (!this.config.enableCache) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const cacheKey = `location_cache_${this.config.cacheKey}`;
|
|
59
|
+
const result = await this.storage.getItem<CachedLocationData | null>(cacheKey, null);
|
|
60
|
+
const cached = unwrap(result, null);
|
|
61
|
+
|
|
62
|
+
if (!cached) {
|
|
63
|
+
this.log("No cached location found");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const cacheAge = now - cached.timestamp;
|
|
69
|
+
const cacheDuration = this.config.cacheDuration || 300000;
|
|
70
|
+
|
|
71
|
+
if (cacheAge > cacheDuration) {
|
|
72
|
+
this.log("Cache expired");
|
|
73
|
+
await this.storage.removeItem(cacheKey);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.log("Using cached location");
|
|
78
|
+
return cached.location;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logError("Cache read error:", error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async cacheLocation(location: LocationData): Promise<void> {
|
|
86
|
+
if (!this.config.enableCache) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const cacheKey = `location_cache_${this.config.cacheKey}`;
|
|
92
|
+
const cachedData: CachedLocationData = {
|
|
93
|
+
location,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
await this.storage.setItem(cacheKey, cachedData);
|
|
97
|
+
this.log("Location cached successfully");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.logError("Cache write error:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getCurrentPosition(): Promise<LocationData> {
|
|
104
|
+
const withAddress = this.config.withAddress ?? true;
|
|
105
|
+
|
|
106
|
+
this.log("getCurrentPosition called");
|
|
107
|
+
|
|
108
|
+
const cached = await this.getCachedLocation();
|
|
109
|
+
if (cached) {
|
|
110
|
+
this.log("Returning cached location");
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
|
|
27
114
|
const hasPermission = await this.requestPermissions();
|
|
28
115
|
if (!hasPermission) {
|
|
29
|
-
|
|
30
|
-
throw
|
|
116
|
+
this.logWarn("Permission denied");
|
|
117
|
+
throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
|
|
31
118
|
}
|
|
32
119
|
|
|
33
120
|
try {
|
|
34
|
-
|
|
121
|
+
this.log("Getting position...");
|
|
35
122
|
const location = await Location.getCurrentPositionAsync({
|
|
36
|
-
accuracy:
|
|
123
|
+
accuracy: this.config.accuracy,
|
|
37
124
|
});
|
|
38
|
-
|
|
125
|
+
this.log("Position obtained", location);
|
|
39
126
|
|
|
40
127
|
let addressData;
|
|
41
128
|
if (withAddress) {
|
|
42
|
-
|
|
129
|
+
this.log("Reverse geocoding...");
|
|
43
130
|
addressData = await this.reverseGeocode(
|
|
44
131
|
location.coords.latitude,
|
|
45
132
|
location.coords.longitude
|
|
46
133
|
);
|
|
47
|
-
|
|
134
|
+
this.log("Address obtained", addressData);
|
|
48
135
|
}
|
|
49
136
|
|
|
50
|
-
|
|
137
|
+
const locationData: LocationData = {
|
|
51
138
|
coords: {
|
|
52
139
|
latitude: location.coords.latitude,
|
|
53
140
|
longitude: location.coords.longitude,
|
|
@@ -55,17 +142,27 @@ export class LocationService {
|
|
|
55
142
|
timestamp: location.timestamp,
|
|
56
143
|
address: addressData,
|
|
57
144
|
};
|
|
145
|
+
|
|
146
|
+
await this.cacheLocation(locationData);
|
|
147
|
+
|
|
148
|
+
return locationData;
|
|
58
149
|
} catch (error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
150
|
+
this.logError("Error getting location:", error);
|
|
151
|
+
|
|
152
|
+
let errorCode: LocationErrorCode = "UNKNOWN_ERROR";
|
|
153
|
+
let errorMessage = "Unknown error getting location";
|
|
154
|
+
|
|
155
|
+
if (error instanceof LocationErrorImpl) {
|
|
156
|
+
errorCode = error.code;
|
|
157
|
+
errorMessage = error.message;
|
|
158
|
+
} else if (error instanceof Error) {
|
|
159
|
+
errorMessage = error.message;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new LocationErrorImpl(errorCode, errorMessage);
|
|
63
163
|
}
|
|
64
164
|
}
|
|
65
165
|
|
|
66
|
-
/**
|
|
67
|
-
* Reverse geocodes coordinates to an address.
|
|
68
|
-
*/
|
|
69
166
|
async reverseGeocode(latitude: number, longitude: number) {
|
|
70
167
|
try {
|
|
71
168
|
const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
|
|
@@ -79,10 +176,61 @@ export class LocationService {
|
|
|
79
176
|
formattedAddress: [address.city, address.country].filter(Boolean).join(", "),
|
|
80
177
|
};
|
|
81
178
|
} catch (error) {
|
|
82
|
-
|
|
179
|
+
this.logWarn("Reverse geocode failed:", error);
|
|
83
180
|
return undefined;
|
|
84
181
|
}
|
|
85
182
|
}
|
|
183
|
+
|
|
184
|
+
async isLocationEnabled(): Promise<boolean> {
|
|
185
|
+
try {
|
|
186
|
+
this.log("Checking if location is enabled...");
|
|
187
|
+
const enabled = await Location.hasServicesEnabledAsync();
|
|
188
|
+
this.log("Location enabled:", enabled);
|
|
189
|
+
return enabled;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.logError("Error checking location enabled:", error);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getPermissionStatus(): Promise<Location.PermissionStatus> {
|
|
197
|
+
try {
|
|
198
|
+
this.log("Getting permission status...");
|
|
199
|
+
const status = await Location.getForegroundPermissionsAsync();
|
|
200
|
+
this.log("Permission status:", status.status);
|
|
201
|
+
return status.status;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
this.logError("Error getting permission status:", error);
|
|
204
|
+
return "undetermined";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getLastKnownPosition(): Promise<LocationData | null> {
|
|
209
|
+
try {
|
|
210
|
+
this.log("Getting last known position...");
|
|
211
|
+
const location = await Location.getLastKnownPositionAsync();
|
|
212
|
+
|
|
213
|
+
if (!location) {
|
|
214
|
+
this.log("No last known position available");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.log("Last known position obtained", location);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
coords: {
|
|
222
|
+
latitude: location.coords.latitude,
|
|
223
|
+
longitude: location.coords.longitude,
|
|
224
|
+
},
|
|
225
|
+
timestamp: location.timestamp,
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logError("Error getting last known position:", error);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
86
232
|
}
|
|
87
233
|
|
|
88
|
-
export
|
|
234
|
+
export function createLocationService(config?: LocationConfig): LocationService {
|
|
235
|
+
return new LocationService(config);
|
|
236
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as Location from "expo-location";
|
|
2
|
+
import {
|
|
3
|
+
LocationData,
|
|
4
|
+
LocationError,
|
|
5
|
+
LocationCallback,
|
|
6
|
+
LocationErrorCallback,
|
|
7
|
+
LocationWatcherOptions,
|
|
8
|
+
LocationErrorImpl,
|
|
9
|
+
LocationErrorCode,
|
|
10
|
+
} from "../../types/location.types";
|
|
11
|
+
|
|
12
|
+
declare const __DEV__: boolean;
|
|
13
|
+
|
|
14
|
+
export class LocationWatcher {
|
|
15
|
+
private subscription: Location.LocationSubscription | null = null;
|
|
16
|
+
private options: LocationWatcherOptions;
|
|
17
|
+
|
|
18
|
+
constructor(options: LocationWatcherOptions = {}) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private log(message: string, ...args: unknown[]): void {
|
|
23
|
+
if (__DEV__) {
|
|
24
|
+
console.log(`[LocationWatcher] ${message}`, ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private logError(message: string, error: unknown): void {
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.error(`[LocationWatcher] ${message}`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async watchPosition(
|
|
35
|
+
onSuccess: LocationCallback,
|
|
36
|
+
onError?: LocationErrorCallback
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
try {
|
|
39
|
+
this.log("Requesting permissions...");
|
|
40
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
41
|
+
|
|
42
|
+
if (status !== "granted") {
|
|
43
|
+
const error: LocationError = {
|
|
44
|
+
code: "PERMISSION_DENIED",
|
|
45
|
+
message: "Location permission not granted",
|
|
46
|
+
};
|
|
47
|
+
if (onError) {
|
|
48
|
+
onError(error);
|
|
49
|
+
}
|
|
50
|
+
throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.log("Starting location watch...");
|
|
54
|
+
|
|
55
|
+
this.subscription = await Location.watchPositionAsync(
|
|
56
|
+
{
|
|
57
|
+
accuracy: this.options.accuracy || Location.Accuracy.Balanced,
|
|
58
|
+
},
|
|
59
|
+
(location) => {
|
|
60
|
+
this.log("Location update received", location);
|
|
61
|
+
|
|
62
|
+
const locationData: LocationData = {
|
|
63
|
+
coords: {
|
|
64
|
+
latitude: location.coords.latitude,
|
|
65
|
+
longitude: location.coords.longitude,
|
|
66
|
+
},
|
|
67
|
+
timestamp: location.timestamp,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
onSuccess(locationData);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return "watching";
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this.logError("Error watching position:", error);
|
|
77
|
+
|
|
78
|
+
let errorCode: LocationErrorCode = "UNKNOWN_ERROR";
|
|
79
|
+
let errorMessage = "Unknown error watching location";
|
|
80
|
+
|
|
81
|
+
if (error instanceof LocationErrorImpl) {
|
|
82
|
+
errorCode = error.code;
|
|
83
|
+
errorMessage = error.message;
|
|
84
|
+
} else if (error instanceof Error) {
|
|
85
|
+
errorMessage = error.message;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const locationError: LocationError = {
|
|
89
|
+
code: errorCode,
|
|
90
|
+
message: errorMessage,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (onError) {
|
|
94
|
+
onError(locationError);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new LocationErrorImpl(errorCode, errorMessage);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clearWatch(): void {
|
|
102
|
+
if (this.subscription) {
|
|
103
|
+
this.log("Clearing location watch");
|
|
104
|
+
this.subscription.remove();
|
|
105
|
+
this.subscription = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isWatching(): boolean {
|
|
110
|
+
return this.subscription !== null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createLocationWatcher(options?: LocationWatcherOptions): LocationWatcher {
|
|
115
|
+
return new LocationWatcher(options);
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Coordinates, DistanceUnit } from "../../types/location.types";
|
|
2
|
+
|
|
3
|
+
export class LocationUtils {
|
|
4
|
+
private static readonly EARTH_RADIUS_KM = 6371;
|
|
5
|
+
private static readonly EARTH_RADIUS_MILES = 3959;
|
|
6
|
+
private static readonly EARTH_RADIUS_METERS = 6371000;
|
|
7
|
+
|
|
8
|
+
static calculateDistance(
|
|
9
|
+
from: Coordinates,
|
|
10
|
+
to: Coordinates,
|
|
11
|
+
unit: DistanceUnit = "km"
|
|
12
|
+
): number {
|
|
13
|
+
const lat1Rad = (from.latitude * Math.PI) / 180;
|
|
14
|
+
const lat2Rad = (to.latitude * Math.PI) / 180;
|
|
15
|
+
const deltaLatRad = ((to.latitude - from.latitude) * Math.PI) / 180;
|
|
16
|
+
const deltaLonRad = ((to.longitude - from.longitude) * Math.PI) / 180;
|
|
17
|
+
|
|
18
|
+
const a =
|
|
19
|
+
Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
|
|
20
|
+
Math.cos(lat1Rad) *
|
|
21
|
+
Math.cos(lat2Rad) *
|
|
22
|
+
Math.sin(deltaLonRad / 2) *
|
|
23
|
+
Math.sin(deltaLonRad / 2);
|
|
24
|
+
|
|
25
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
26
|
+
|
|
27
|
+
switch (unit) {
|
|
28
|
+
case "miles":
|
|
29
|
+
return this.EARTH_RADIUS_MILES * c;
|
|
30
|
+
case "meters":
|
|
31
|
+
return this.EARTH_RADIUS_METERS * c;
|
|
32
|
+
case "km":
|
|
33
|
+
default:
|
|
34
|
+
return this.EARTH_RADIUS_KM * c;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static isValidCoordinate(latitude: number, longitude: number): boolean {
|
|
39
|
+
return (
|
|
40
|
+
latitude >= -90 &&
|
|
41
|
+
latitude <= 90 &&
|
|
42
|
+
longitude >= -180 &&
|
|
43
|
+
longitude <= 180
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static isValidAccuracy(accuracy: number | null | undefined, maxAccuracy = 100): boolean {
|
|
48
|
+
if (accuracy === null || accuracy === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return accuracy > 0 && accuracy <= maxAccuracy;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static formatAccuracy(accuracy: number | null | undefined): string {
|
|
55
|
+
if (accuracy === null || accuracy === undefined) {
|
|
56
|
+
return "Unknown";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (accuracy < 10) {
|
|
60
|
+
return "Excellent (±" + Math.round(accuracy) + "m)";
|
|
61
|
+
}
|
|
62
|
+
if (accuracy < 50) {
|
|
63
|
+
return "Good (±" + Math.round(accuracy) + "m)";
|
|
64
|
+
}
|
|
65
|
+
if (accuracy < 100) {
|
|
66
|
+
return "Fair (±" + Math.round(accuracy) + "m)";
|
|
67
|
+
}
|
|
68
|
+
return "Poor (±" + Math.round(accuracy) + "m)";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static coordinatesAreEqual(
|
|
72
|
+
coord1: Coordinates,
|
|
73
|
+
coord2: Coordinates,
|
|
74
|
+
precision = 6
|
|
75
|
+
): boolean {
|
|
76
|
+
const lat1 = coord1.latitude.toFixed(precision);
|
|
77
|
+
const lat2 = coord2.latitude.toFixed(precision);
|
|
78
|
+
const lon1 = coord1.longitude.toFixed(precision);
|
|
79
|
+
const lon2 = coord2.longitude.toFixed(precision);
|
|
80
|
+
|
|
81
|
+
return lat1 === lat2 && lon1 === lon2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { LocationData, LocationError } from "../../
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
import { createLocationService } from "../../infrastructure/services/LocationService";
|
|
3
|
+
import { LocationData, LocationError, LocationConfig } from "../../types/location.types";
|
|
4
4
|
|
|
5
5
|
export interface UseLocationResult {
|
|
6
6
|
location: LocationData | null;
|
|
@@ -9,7 +9,8 @@ export interface UseLocationResult {
|
|
|
9
9
|
getCurrentLocation: () => Promise<LocationData | null>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function useLocation(): UseLocationResult {
|
|
12
|
+
export function useLocation(config?: LocationConfig): UseLocationResult {
|
|
13
|
+
const serviceRef = useRef(createLocationService(config));
|
|
13
14
|
const [location, setLocation] = useState<LocationData | null>(null);
|
|
14
15
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
16
|
const [error, setError] = useState<LocationError | null>(null);
|
|
@@ -17,15 +18,24 @@ export function useLocation(): UseLocationResult {
|
|
|
17
18
|
const getCurrentLocation = useCallback(async () => {
|
|
18
19
|
setIsLoading(true);
|
|
19
20
|
setError(null);
|
|
21
|
+
|
|
20
22
|
try {
|
|
21
|
-
const data = await
|
|
23
|
+
const data = await serviceRef.current.getCurrentPosition();
|
|
22
24
|
setLocation(data);
|
|
23
25
|
return data;
|
|
24
|
-
} catch (err
|
|
25
|
-
|
|
26
|
-
code:
|
|
27
|
-
message:
|
|
26
|
+
} catch (err) {
|
|
27
|
+
let errorObj: LocationError = {
|
|
28
|
+
code: "UNKNOWN_ERROR",
|
|
29
|
+
message: "An unknown error occurred",
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
33
|
+
errorObj = {
|
|
34
|
+
code: typeof err.code === "string" ? err.code : "UNKNOWN_ERROR",
|
|
35
|
+
message: typeof err.message === "string" ? err.message : "An unknown error occurred",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
setError(errorObj);
|
|
30
40
|
return null;
|
|
31
41
|
} finally {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
2
|
+
import { createLocationWatcher } from "../../infrastructure/services/LocationWatcher";
|
|
3
|
+
import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
|
|
4
|
+
|
|
5
|
+
export interface UseLocationWatchResult {
|
|
6
|
+
location: LocationData | null;
|
|
7
|
+
error: LocationError | null;
|
|
8
|
+
isWatching: boolean;
|
|
9
|
+
startWatching: () => Promise<void>;
|
|
10
|
+
stopWatching: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useLocationWatch(options?: LocationWatcherOptions): UseLocationWatchResult {
|
|
14
|
+
const watcherRef = useRef<ReturnType<typeof createLocationWatcher> | null>(null);
|
|
15
|
+
const [location, setLocation] = useState<LocationData | null>(null);
|
|
16
|
+
const [error, setError] = useState<LocationError | null>(null);
|
|
17
|
+
const [isWatching, setIsWatching] = useState(false);
|
|
18
|
+
|
|
19
|
+
const stopWatching = useCallback(() => {
|
|
20
|
+
if (watcherRef.current) {
|
|
21
|
+
watcherRef.current.clearWatch();
|
|
22
|
+
watcherRef.current = null;
|
|
23
|
+
setIsWatching(false);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const startWatching = useCallback(async () => {
|
|
28
|
+
stopWatching();
|
|
29
|
+
|
|
30
|
+
const watcher = createLocationWatcher(options);
|
|
31
|
+
watcherRef.current = watcher;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await watcher.watchPosition(
|
|
35
|
+
(data) => {
|
|
36
|
+
setLocation(data);
|
|
37
|
+
setError(null);
|
|
38
|
+
},
|
|
39
|
+
(err) => {
|
|
40
|
+
setError(err);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
setIsWatching(true);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
let errorObj: LocationError = {
|
|
46
|
+
code: "UNKNOWN_ERROR",
|
|
47
|
+
message: "An unknown error occurred",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
51
|
+
errorObj = {
|
|
52
|
+
code: typeof err.code === "string" ? err.code : "UNKNOWN_ERROR",
|
|
53
|
+
message: typeof err.message === "string" ? err.message : "An unknown error occurred",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setError(errorObj);
|
|
58
|
+
setIsWatching(false);
|
|
59
|
+
}
|
|
60
|
+
}, [options, stopWatching]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
stopWatching();
|
|
65
|
+
};
|
|
66
|
+
}, [stopWatching]);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
location,
|
|
70
|
+
error,
|
|
71
|
+
isWatching,
|
|
72
|
+
startWatching,
|
|
73
|
+
stopWatching,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as Location from "expo-location";
|
|
2
|
+
|
|
3
|
+
export interface Coordinates {
|
|
4
|
+
latitude: number;
|
|
5
|
+
longitude: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LocationAddress {
|
|
9
|
+
city?: string | null;
|
|
10
|
+
region?: string | null;
|
|
11
|
+
country?: string | null;
|
|
12
|
+
street?: string | null;
|
|
13
|
+
formattedAddress?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LocationData {
|
|
17
|
+
coords: Coordinates;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
address?: LocationAddress;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LocationError {
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CachedLocationData {
|
|
28
|
+
location: LocationData;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type DistanceUnit = "km" | "miles" | "meters";
|
|
33
|
+
|
|
34
|
+
export type LocationErrorCode =
|
|
35
|
+
| "PERMISSION_DENIED"
|
|
36
|
+
| "LOCATION_UNAVAILABLE"
|
|
37
|
+
| "TIMEOUT"
|
|
38
|
+
| "CACHE_ERROR"
|
|
39
|
+
| "UNKNOWN_ERROR";
|
|
40
|
+
|
|
41
|
+
export interface LocationConfig {
|
|
42
|
+
accuracy?: Location.Accuracy;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
enableCache?: boolean;
|
|
45
|
+
cacheKey?: string;
|
|
46
|
+
cacheDuration?: number;
|
|
47
|
+
withAddress?: boolean;
|
|
48
|
+
distanceFilter?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_LOCATION_CONFIG: LocationConfig = {
|
|
52
|
+
accuracy: Location.Accuracy.Balanced,
|
|
53
|
+
timeout: 10000,
|
|
54
|
+
enableCache: true,
|
|
55
|
+
cacheKey: "default",
|
|
56
|
+
cacheDuration: 300000,
|
|
57
|
+
withAddress: true,
|
|
58
|
+
distanceFilter: 10,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type LocationCallback = (location: LocationData) => void;
|
|
62
|
+
export type LocationErrorCallback = (error: LocationError) => void;
|
|
63
|
+
|
|
64
|
+
export interface LocationWatcherOptions {
|
|
65
|
+
accuracy?: Location.Accuracy;
|
|
66
|
+
distanceFilter?: number;
|
|
67
|
+
timeout?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class LocationErrorImpl extends Error implements LocationError {
|
|
71
|
+
code: LocationErrorCode;
|
|
72
|
+
|
|
73
|
+
constructor(code: LocationErrorCode, message: string) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "LocationError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
}
|