@umituz/react-native-location 1.0.0
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/LICENSE +22 -0
- package/README.md +127 -0
- package/lib/domain/entities/Location.d.ts +144 -0
- package/lib/domain/entities/Location.d.ts.map +1 -0
- package/lib/domain/entities/Location.js +52 -0
- package/lib/domain/entities/Location.js.map +1 -0
- package/lib/index.d.ts +95 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +95 -0
- package/lib/index.js.map +1 -0
- package/lib/infrastructure/services/LocationService.d.ts +93 -0
- package/lib/infrastructure/services/LocationService.d.ts.map +1 -0
- package/lib/infrastructure/services/LocationService.js +250 -0
- package/lib/infrastructure/services/LocationService.js.map +1 -0
- package/lib/presentation/hooks/useLocation.d.ts +95 -0
- package/lib/presentation/hooks/useLocation.d.ts.map +1 -0
- package/lib/presentation/hooks/useLocation.js +156 -0
- package/lib/presentation/hooks/useLocation.js.map +1 -0
- package/package.json +57 -0
- package/src/domain/entities/Location.ts +175 -0
- package/src/index.ts +107 -0
- package/src/infrastructure/services/LocationService.ts +298 -0
- package/src/presentation/hooks/useLocation.ts +225 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location Domain Entities
|
|
3
|
+
*
|
|
4
|
+
* Core location types and interfaces for device location services
|
|
5
|
+
* and geolocation features
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Current location retrieval with GPS
|
|
9
|
+
* - Location permission management
|
|
10
|
+
* - Location caching (15-minute cache)
|
|
11
|
+
* - Reverse geocoding (address lookup)
|
|
12
|
+
* - Fallback strategies (High → Balanced → Cached)
|
|
13
|
+
* - Platform-aware (iOS/Android only, no web)
|
|
14
|
+
*
|
|
15
|
+
* Dependencies:
|
|
16
|
+
* - expo-location (GPS and permissions)
|
|
17
|
+
* - AsyncStorage (location cache)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Location data with coordinates and optional address
|
|
22
|
+
*/
|
|
23
|
+
export interface LocationData {
|
|
24
|
+
/** Latitude coordinate */
|
|
25
|
+
latitude: number;
|
|
26
|
+
|
|
27
|
+
/** Longitude coordinate */
|
|
28
|
+
longitude: number;
|
|
29
|
+
|
|
30
|
+
/** Human-readable address (reverse geocoded) */
|
|
31
|
+
address?: string;
|
|
32
|
+
|
|
33
|
+
/** Timestamp when location was captured (milliseconds since epoch) */
|
|
34
|
+
timestamp: number;
|
|
35
|
+
|
|
36
|
+
/** GPS accuracy in meters (lower is better) */
|
|
37
|
+
accuracy?: number;
|
|
38
|
+
|
|
39
|
+
/** Whether this location is from cache (not fresh GPS) */
|
|
40
|
+
isCached?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Location permission status
|
|
45
|
+
*/
|
|
46
|
+
export type LocationPermissionStatus = 'granted' | 'denied' | 'unknown';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Location accuracy levels for GPS
|
|
50
|
+
*/
|
|
51
|
+
export enum LocationAccuracy {
|
|
52
|
+
/** Lowest accuracy, fastest response, battery-friendly */
|
|
53
|
+
Lowest = 1,
|
|
54
|
+
|
|
55
|
+
/** Low accuracy */
|
|
56
|
+
Low = 2,
|
|
57
|
+
|
|
58
|
+
/** Balanced accuracy (recommended for most use cases) */
|
|
59
|
+
Balanced = 3,
|
|
60
|
+
|
|
61
|
+
/** High accuracy (GPS-based) */
|
|
62
|
+
High = 4,
|
|
63
|
+
|
|
64
|
+
/** Highest accuracy (most battery-intensive) */
|
|
65
|
+
Highest = 5,
|
|
66
|
+
|
|
67
|
+
/** Best for navigation (continuous high accuracy) */
|
|
68
|
+
BestForNavigation = 6,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Location cache configuration
|
|
73
|
+
*/
|
|
74
|
+
export interface LocationCacheConfig {
|
|
75
|
+
/** Cache duration in milliseconds (default: 15 minutes) */
|
|
76
|
+
duration: number;
|
|
77
|
+
|
|
78
|
+
/** Storage key for cached location */
|
|
79
|
+
storageKey: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Location retrieval configuration
|
|
84
|
+
*/
|
|
85
|
+
export interface LocationRetrievalConfig {
|
|
86
|
+
/** Timeout for high accuracy request (milliseconds) */
|
|
87
|
+
highAccuracyTimeout: number;
|
|
88
|
+
|
|
89
|
+
/** Timeout for balanced accuracy request (milliseconds) */
|
|
90
|
+
balancedAccuracyTimeout: number;
|
|
91
|
+
|
|
92
|
+
/** Whether to enable reverse geocoding (address lookup) */
|
|
93
|
+
enableGeocoding: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Cached location storage format
|
|
98
|
+
*/
|
|
99
|
+
export interface CachedLocation {
|
|
100
|
+
/** Location data */
|
|
101
|
+
data: LocationData;
|
|
102
|
+
|
|
103
|
+
/** Timestamp when location was cached (milliseconds since epoch) */
|
|
104
|
+
cachedAt: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Location Service Interface
|
|
109
|
+
* Defines all location-related operations
|
|
110
|
+
*/
|
|
111
|
+
export interface ILocationService {
|
|
112
|
+
/**
|
|
113
|
+
* Request location permissions from user
|
|
114
|
+
* @returns Promise resolving to true if granted, false otherwise
|
|
115
|
+
*/
|
|
116
|
+
requestPermissions(): Promise<boolean>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if location permissions are granted
|
|
120
|
+
* @returns Promise resolving to true if granted, false otherwise
|
|
121
|
+
*/
|
|
122
|
+
hasPermissions(): Promise<boolean>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get current permission status without requesting
|
|
126
|
+
* @returns Current permission status ('granted' | 'denied' | 'unknown')
|
|
127
|
+
*/
|
|
128
|
+
getPermissionStatus(): LocationPermissionStatus;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get current device location with fallback strategies
|
|
132
|
+
* Priority: High accuracy → Balanced accuracy → Cached location → null
|
|
133
|
+
* @returns Promise resolving to LocationData or null if unavailable
|
|
134
|
+
*/
|
|
135
|
+
getCurrentLocation(): Promise<LocationData | null>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get cached location if available and not expired
|
|
139
|
+
* @returns Cached location or null
|
|
140
|
+
*/
|
|
141
|
+
getCachedLocation(): LocationData | null;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format location for display (address or coordinates)
|
|
145
|
+
* @param location - LocationData to format
|
|
146
|
+
* @returns Formatted string (address or "lat, lng")
|
|
147
|
+
*/
|
|
148
|
+
formatLocation(location: LocationData): string;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if location services are available on this platform
|
|
152
|
+
* @returns True if available (iOS/Android), false otherwise (web)
|
|
153
|
+
*/
|
|
154
|
+
isLocationAvailable(): boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Location service constants
|
|
159
|
+
*/
|
|
160
|
+
export const LOCATION_CONSTANTS = {
|
|
161
|
+
/** Cache duration: 15 minutes */
|
|
162
|
+
CACHE_DURATION: 15 * 60 * 1000,
|
|
163
|
+
|
|
164
|
+
/** High accuracy timeout: 15 seconds */
|
|
165
|
+
HIGH_ACCURACY_TIMEOUT: 15000,
|
|
166
|
+
|
|
167
|
+
/** Balanced accuracy timeout: 8 seconds */
|
|
168
|
+
BALANCED_ACCURACY_TIMEOUT: 8000,
|
|
169
|
+
|
|
170
|
+
/** AsyncStorage key for cached location */
|
|
171
|
+
CACHE_KEY: '@location_cache',
|
|
172
|
+
|
|
173
|
+
/** Coordinate decimal precision for display */
|
|
174
|
+
COORDINATE_PRECISION: 4,
|
|
175
|
+
} as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location Domain - Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Global infrastructure domain for device location services and geolocation
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Current location retrieval with GPS
|
|
8
|
+
* - Location permission management (iOS/Android)
|
|
9
|
+
* - 15-minute location caching for performance
|
|
10
|
+
* - Reverse geocoding (coordinates → address)
|
|
11
|
+
* - Multi-tier accuracy fallback (High → Balanced → Cached)
|
|
12
|
+
* - Platform-aware (iOS/Android only, no web support)
|
|
13
|
+
* - Silent failures (no console logging)
|
|
14
|
+
*
|
|
15
|
+
* Dependencies:
|
|
16
|
+
* - expo-location (GPS and permissions API)
|
|
17
|
+
* - AsyncStorage (cache persistence)
|
|
18
|
+
*
|
|
19
|
+
* USAGE:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Recommended: Use hook in components
|
|
22
|
+
* import { useLocation } from '@umituz/react-native-location';
|
|
23
|
+
*
|
|
24
|
+
* const MyComponent = () => {
|
|
25
|
+
* const {
|
|
26
|
+
* location,
|
|
27
|
+
* loading,
|
|
28
|
+
* hasPermission,
|
|
29
|
+
* requestPermission,
|
|
30
|
+
* getCurrentLocation,
|
|
31
|
+
* formatLocation,
|
|
32
|
+
* } = useLocation();
|
|
33
|
+
*
|
|
34
|
+
* useEffect(() => {
|
|
35
|
+
* // Auto-fetch on mount
|
|
36
|
+
* getCurrentLocation();
|
|
37
|
+
* }, []);
|
|
38
|
+
*
|
|
39
|
+
* if (loading) return <LoadingIndicator />;
|
|
40
|
+
*
|
|
41
|
+
* return (
|
|
42
|
+
* <View>
|
|
43
|
+
* {location && (
|
|
44
|
+
* <>
|
|
45
|
+
* <Text>Location: {formatLocation(location)}</Text>
|
|
46
|
+
* <Text>Lat: {location.latitude}</Text>
|
|
47
|
+
* <Text>Lng: {location.longitude}</Text>
|
|
48
|
+
* {location.accuracy && (
|
|
49
|
+
* <Text>Accuracy: {location.accuracy.toFixed(0)}m</Text>
|
|
50
|
+
* )}
|
|
51
|
+
* </>
|
|
52
|
+
* )}
|
|
53
|
+
* <AtomicButton onPress={getCurrentLocation}>
|
|
54
|
+
* Refresh Location
|
|
55
|
+
* </AtomicButton>
|
|
56
|
+
* </View>
|
|
57
|
+
* );
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* // Alternative: Use service directly (for non-component code)
|
|
61
|
+
* import { locationService } from '@umituz/react-native-location';
|
|
62
|
+
*
|
|
63
|
+
* const location = await locationService.getCurrentLocation();
|
|
64
|
+
* if (location) {
|
|
65
|
+
* console.log(locationService.formatLocation(location));
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* PLATFORM SUPPORT:
|
|
70
|
+
* - ✅ iOS: Full support (GPS, permissions, geocoding)
|
|
71
|
+
* - ✅ Android: Full support (GPS, permissions, geocoding)
|
|
72
|
+
* - ❌ Web: Not supported (returns null)
|
|
73
|
+
*
|
|
74
|
+
* PERMISSION FLOW:
|
|
75
|
+
* 1. Check: hasPermissions() → true/false
|
|
76
|
+
* 2. Request: requestPermissions() → true/false
|
|
77
|
+
* 3. Retrieve: getCurrentLocation() → LocationData | null
|
|
78
|
+
*
|
|
79
|
+
* FALLBACK STRATEGY:
|
|
80
|
+
* 1. High accuracy GPS (15s timeout)
|
|
81
|
+
* 2. Balanced accuracy GPS (8s timeout)
|
|
82
|
+
* 3. Cached location (15min cache)
|
|
83
|
+
* 4. null (no location available)
|
|
84
|
+
*
|
|
85
|
+
* CACHING:
|
|
86
|
+
* - Duration: 15 minutes
|
|
87
|
+
* - Storage: AsyncStorage (@location_cache)
|
|
88
|
+
* - Auto-refresh: On getCurrentLocation() if expired
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
// Domain Entities
|
|
92
|
+
export type {
|
|
93
|
+
LocationData,
|
|
94
|
+
LocationPermissionStatus,
|
|
95
|
+
CachedLocation,
|
|
96
|
+
LocationCacheConfig,
|
|
97
|
+
LocationRetrievalConfig,
|
|
98
|
+
ILocationService,
|
|
99
|
+
} from './domain/entities/Location';
|
|
100
|
+
export { LocationAccuracy, LOCATION_CONSTANTS } from './domain/entities/Location';
|
|
101
|
+
|
|
102
|
+
// Infrastructure Services
|
|
103
|
+
export { locationService } from './infrastructure/services/LocationService';
|
|
104
|
+
|
|
105
|
+
// Presentation Hooks
|
|
106
|
+
export { useLocation } from './presentation/hooks/useLocation';
|
|
107
|
+
export type { UseLocationReturn } from './presentation/hooks/useLocation';
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location Service Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles device location retrieval, permissions, caching, and reverse geocoding
|
|
5
|
+
* with intelligent fallback strategies
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multi-tier accuracy fallback (High → Balanced → Cached)
|
|
9
|
+
* - 15-minute location caching for performance
|
|
10
|
+
* - Reverse geocoding with coordinate fallback
|
|
11
|
+
* - Platform detection (iOS/Android only, no web)
|
|
12
|
+
* - Silent failures (no console logging)
|
|
13
|
+
* - AsyncStorage persistence
|
|
14
|
+
*
|
|
15
|
+
* Dependencies:
|
|
16
|
+
* - expo-location (GPS and permissions API)
|
|
17
|
+
* - AsyncStorage (cache persistence)
|
|
18
|
+
*
|
|
19
|
+
* USAGE:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { locationService } from '@umituz/react-native-location';
|
|
22
|
+
*
|
|
23
|
+
* // Check permission
|
|
24
|
+
* const hasPermission = await locationService.hasPermissions();
|
|
25
|
+
*
|
|
26
|
+
* // Request permission if needed
|
|
27
|
+
* if (!hasPermission) {
|
|
28
|
+
* const granted = await locationService.requestPermissions();
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* // Get current location
|
|
32
|
+
* const location = await locationService.getCurrentLocation();
|
|
33
|
+
* if (location) {
|
|
34
|
+
* console.log(location.latitude, location.longitude);
|
|
35
|
+
* console.log(locationService.formatLocation(location));
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { Platform } from 'react-native';
|
|
41
|
+
import * as Location from 'expo-location';
|
|
42
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
43
|
+
|
|
44
|
+
import type {
|
|
45
|
+
LocationData,
|
|
46
|
+
LocationPermissionStatus,
|
|
47
|
+
CachedLocation,
|
|
48
|
+
ILocationService,
|
|
49
|
+
} from '../../domain/entities/Location';
|
|
50
|
+
import { LOCATION_CONSTANTS } from '../../domain/entities/Location';
|
|
51
|
+
|
|
52
|
+
class LocationService implements ILocationService {
|
|
53
|
+
private cachedLocation: LocationData | null = null;
|
|
54
|
+
private cachedAt: number = 0;
|
|
55
|
+
private permissionStatus: LocationPermissionStatus = 'unknown';
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
this.loadCachedLocation();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if location services are available on this platform
|
|
63
|
+
*/
|
|
64
|
+
isLocationAvailable(): boolean {
|
|
65
|
+
// Location services not available on web
|
|
66
|
+
if (Platform.OS === 'web') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load cached location from AsyncStorage
|
|
74
|
+
*/
|
|
75
|
+
private async loadCachedLocation(): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const cached = await AsyncStorage.getItem(LOCATION_CONSTANTS.CACHE_KEY);
|
|
78
|
+
if (cached) {
|
|
79
|
+
const parsed: CachedLocation = JSON.parse(cached);
|
|
80
|
+
if (Date.now() - parsed.cachedAt < LOCATION_CONSTANTS.CACHE_DURATION) {
|
|
81
|
+
this.cachedLocation = parsed.data;
|
|
82
|
+
this.cachedAt = parsed.cachedAt;
|
|
83
|
+
} else {
|
|
84
|
+
// Expired cache, remove it
|
|
85
|
+
await AsyncStorage.removeItem(LOCATION_CONSTANTS.CACHE_KEY);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Silent failure on cache load
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Save location to cache (AsyncStorage + memory)
|
|
95
|
+
*/
|
|
96
|
+
private async saveCachedLocation(location: LocationData): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const cached: CachedLocation = {
|
|
99
|
+
data: location,
|
|
100
|
+
cachedAt: Date.now(),
|
|
101
|
+
};
|
|
102
|
+
this.cachedLocation = location;
|
|
103
|
+
this.cachedAt = cached.cachedAt;
|
|
104
|
+
await AsyncStorage.setItem(
|
|
105
|
+
LOCATION_CONSTANTS.CACHE_KEY,
|
|
106
|
+
JSON.stringify(cached)
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Silent failure on cache save
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get cached location if available and not expired
|
|
115
|
+
*/
|
|
116
|
+
getCachedLocation(): LocationData | null {
|
|
117
|
+
if (!this.cachedLocation) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const age = Date.now() - this.cachedAt;
|
|
121
|
+
if (age < LOCATION_CONSTANTS.CACHE_DURATION) {
|
|
122
|
+
return { ...this.cachedLocation, isCached: true };
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Request location permissions from user
|
|
129
|
+
*/
|
|
130
|
+
async requestPermissions(): Promise<boolean> {
|
|
131
|
+
if (!this.isLocationAvailable()) {
|
|
132
|
+
this.permissionStatus = 'denied';
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
138
|
+
this.permissionStatus = status === 'granted' ? 'granted' : 'denied';
|
|
139
|
+
return status === 'granted';
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.permissionStatus = 'denied';
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if location permissions are granted
|
|
148
|
+
*/
|
|
149
|
+
async hasPermissions(): Promise<boolean> {
|
|
150
|
+
if (!this.isLocationAvailable()) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Use cached permission status if available
|
|
155
|
+
if (this.permissionStatus !== 'unknown') {
|
|
156
|
+
return this.permissionStatus === 'granted';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const { status } = await Location.getForegroundPermissionsAsync();
|
|
161
|
+
this.permissionStatus = status === 'granted' ? 'granted' : 'denied';
|
|
162
|
+
return status === 'granted';
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.permissionStatus = 'denied';
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get permission status without requesting
|
|
171
|
+
*/
|
|
172
|
+
getPermissionStatus(): LocationPermissionStatus {
|
|
173
|
+
return this.permissionStatus;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get current location with fallback strategies
|
|
178
|
+
* Priority: High accuracy → Balanced accuracy → Cached location → null
|
|
179
|
+
*/
|
|
180
|
+
async getCurrentLocation(): Promise<LocationData | null> {
|
|
181
|
+
if (!this.isLocationAvailable()) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const hasPermission = await this.hasPermissions();
|
|
187
|
+
|
|
188
|
+
if (!hasPermission) {
|
|
189
|
+
const granted = await this.requestPermissions();
|
|
190
|
+
if (!granted) {
|
|
191
|
+
// Return cached location if available
|
|
192
|
+
return this.getCachedLocation();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try high accuracy first
|
|
197
|
+
let locationData = await this.getLocationWithAccuracy(
|
|
198
|
+
Location.Accuracy.High,
|
|
199
|
+
LOCATION_CONSTANTS.HIGH_ACCURACY_TIMEOUT
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Fallback to balanced accuracy if high accuracy fails
|
|
203
|
+
if (!locationData) {
|
|
204
|
+
locationData = await this.getLocationWithAccuracy(
|
|
205
|
+
Location.Accuracy.Balanced,
|
|
206
|
+
LOCATION_CONSTANTS.BALANCED_ACCURACY_TIMEOUT
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fallback to cached location
|
|
211
|
+
if (!locationData) {
|
|
212
|
+
return this.getCachedLocation();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Cache the new location
|
|
216
|
+
await this.saveCachedLocation(locationData);
|
|
217
|
+
|
|
218
|
+
return locationData;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Return cached location as last resort
|
|
221
|
+
return this.getCachedLocation();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get location with specific accuracy and timeout
|
|
227
|
+
*/
|
|
228
|
+
private async getLocationWithAccuracy(
|
|
229
|
+
accuracy: Location.LocationAccuracy,
|
|
230
|
+
timeout: number
|
|
231
|
+
): Promise<LocationData | null> {
|
|
232
|
+
try {
|
|
233
|
+
const location = (await Promise.race([
|
|
234
|
+
Location.getCurrentPositionAsync({ accuracy }),
|
|
235
|
+
new Promise<null>((_, reject) =>
|
|
236
|
+
setTimeout(() => reject(new Error('Timeout')), timeout)
|
|
237
|
+
),
|
|
238
|
+
])) as Location.LocationObject | null;
|
|
239
|
+
|
|
240
|
+
if (!location) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const locationData: LocationData = {
|
|
245
|
+
latitude: location.coords.latitude,
|
|
246
|
+
longitude: location.coords.longitude,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
accuracy: location.coords.accuracy ?? undefined,
|
|
249
|
+
isCached: false,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Try to get address (reverse geocoding)
|
|
253
|
+
try {
|
|
254
|
+
const addresses = await Location.reverseGeocodeAsync({
|
|
255
|
+
latitude: location.coords.latitude,
|
|
256
|
+
longitude: location.coords.longitude,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (addresses && addresses.length > 0) {
|
|
260
|
+
const address = addresses[0];
|
|
261
|
+
const addressParts = [address.street, address.city, address.region].filter(
|
|
262
|
+
Boolean
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
locationData.address =
|
|
266
|
+
addressParts.join(', ') || 'Unknown location';
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Fallback to coordinates if geocoding fails
|
|
270
|
+
locationData.address = `${location.coords.latitude.toFixed(
|
|
271
|
+
LOCATION_CONSTANTS.COORDINATE_PRECISION
|
|
272
|
+
)}, ${location.coords.longitude.toFixed(LOCATION_CONSTANTS.COORDINATE_PRECISION)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return locationData;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Format location for display (address or coordinates)
|
|
283
|
+
*/
|
|
284
|
+
formatLocation(location: LocationData): string {
|
|
285
|
+
if (location.address) {
|
|
286
|
+
return location.address;
|
|
287
|
+
}
|
|
288
|
+
return `${location.latitude.toFixed(
|
|
289
|
+
LOCATION_CONSTANTS.COORDINATE_PRECISION
|
|
290
|
+
)}, ${location.longitude.toFixed(LOCATION_CONSTANTS.COORDINATE_PRECISION)}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Singleton instance
|
|
296
|
+
* Use this for all location operations
|
|
297
|
+
*/
|
|
298
|
+
export const locationService = new LocationService();
|