import { EventEmitter, Platform, CodedError } from '@unimodules/core'; import invariant from 'invariant'; import ExpoLocation from './ExpoLocation'; const LocationEventEmitter = new EventEmitter(ExpoLocation); var LocationAccuracy; (function (LocationAccuracy) { LocationAccuracy[LocationAccuracy["Lowest"] = 1] = "Lowest"; LocationAccuracy[LocationAccuracy["Low"] = 2] = "Low"; LocationAccuracy[LocationAccuracy["Balanced"] = 3] = "Balanced"; LocationAccuracy[LocationAccuracy["High"] = 4] = "High"; LocationAccuracy[LocationAccuracy["Highest"] = 5] = "Highest"; LocationAccuracy[LocationAccuracy["BestForNavigation"] = 6] = "BestForNavigation"; })(LocationAccuracy || (LocationAccuracy = {})); var LocationActivityType; (function (LocationActivityType) { LocationActivityType[LocationActivityType["Other"] = 1] = "Other"; LocationActivityType[LocationActivityType["AutomotiveNavigation"] = 2] = "AutomotiveNavigation"; LocationActivityType[LocationActivityType["Fitness"] = 3] = "Fitness"; LocationActivityType[LocationActivityType["OtherNavigation"] = 4] = "OtherNavigation"; LocationActivityType[LocationActivityType["Airborne"] = 5] = "Airborne"; })(LocationActivityType || (LocationActivityType = {})); export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType }; export var GeofencingEventType; (function (GeofencingEventType) { GeofencingEventType[GeofencingEventType["Enter"] = 1] = "Enter"; GeofencingEventType[GeofencingEventType["Exit"] = 2] = "Exit"; })(GeofencingEventType || (GeofencingEventType = {})); export var GeofencingRegionState; (function (GeofencingRegionState) { GeofencingRegionState[GeofencingRegionState["Unknown"] = 0] = "Unknown"; GeofencingRegionState[GeofencingRegionState["Inside"] = 1] = "Inside"; GeofencingRegionState[GeofencingRegionState["Outside"] = 2] = "Outside"; })(GeofencingRegionState || (GeofencingRegionState = {})); let nextWatchId = 0; let headingId; function _getNextWatchId() { nextWatchId++; return nextWatchId; } function _getCurrentWatchId() { return nextWatchId; } let watchCallbacks = {}; let deviceEventSubscription; let headingEventSub; let googleApiKey; const googleApiUrl = 'https://maps.googleapis.com/maps/api/geocode/json'; export async function getProviderStatusAsync() { return ExpoLocation.getProviderStatusAsync(); } export async function enableNetworkProviderAsync() { // If network provider is disabled (user's location mode is set to "Device only"), // Android's location provider may not give you any results. Use this method in order to ask the user // to change the location mode to "High accuracy" which uses Google Play services and enables network provider. // `getCurrentPositionAsync` and `watchPositionAsync` are doing it automatically anyway. if (Platform.OS === 'android') { return ExpoLocation.enableNetworkProviderAsync(); } } export async function getCurrentPositionAsync(options = {}) { return ExpoLocation.getCurrentPositionAsync(options); } // Start Compass Module // To simplify, we will call watchHeadingAsync and wait for one update To ensure accuracy, we wait // for a couple of watch updates if the data has low accuracy export async function getHeadingAsync() { return new Promise(async (resolve, reject) => { try { // If there is already a compass active (would be a watch) if (headingEventSub) { let tries = 0; const headingSub = LocationEventEmitter.addListener('Expo.headingChanged', ({ heading }) => { if (heading.accuracy > 1 || tries > 5) { resolve(heading); LocationEventEmitter.removeSubscription(headingSub); } else { tries += 1; } }); } else { let done = false; let subscription; let tries = 0; subscription = await watchHeadingAsync((heading) => { if (!done) { if (heading.accuracy > 1 || tries > 5) { subscription.remove(); resolve(heading); done = true; } else { tries += 1; } } else { subscription.remove(); } }); if (done) { subscription.remove(); } } } catch (e) { reject(e); } }); } export async function watchHeadingAsync(callback) { // Check if there is already a compass event watch. if (headingEventSub) { _removeHeadingWatcher(headingId); } headingEventSub = LocationEventEmitter.addListener('Expo.headingChanged', ({ watchId, heading }) => { const callback = watchCallbacks[watchId]; if (callback) { callback(heading); } else { ExpoLocation.removeWatchAsync(watchId); } }); headingId = _getNextWatchId(); watchCallbacks[headingId] = callback; await ExpoLocation.watchDeviceHeading(headingId); return { remove() { _removeHeadingWatcher(headingId); }, }; } // Removes the compass listener and sub from JS and Native function _removeHeadingWatcher(watchId) { if (!watchCallbacks[watchId]) { return; } delete watchCallbacks[watchId]; ExpoLocation.removeWatchAsync(watchId); if (headingEventSub) { LocationEventEmitter.removeSubscription(headingEventSub); headingEventSub = null; } } // End Compass Module function _maybeInitializeEmitterSubscription() { if (!deviceEventSubscription) { deviceEventSubscription = LocationEventEmitter.addListener('Expo.locationChanged', ({ watchId, location }) => { const callback = watchCallbacks[watchId]; if (callback) { callback(location); } else { ExpoLocation.removeWatchAsync(watchId); } }); } } export async function geocodeAsync(address) { return ExpoLocation.geocodeAsync(address).catch(error => { const platformUsesGoogleMaps = Platform.OS === 'android' || Platform.OS === 'web'; if (platformUsesGoogleMaps && error.code === 'E_NO_GEOCODER') { if (!googleApiKey) { throw new CodedError(error.code, `${error.message} Please set a Google API Key to use geocoding.`); } return _googleGeocodeAsync(address); } throw error; }); } export async function reverseGeocodeAsync(location) { if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number') { throw new TypeError('Location should be an object with number properties `latitude` and `longitude`.'); } return ExpoLocation.reverseGeocodeAsync(location).catch(error => { const platformUsesGoogleMaps = Platform.OS === 'android' || Platform.OS === 'web'; if (platformUsesGoogleMaps && error.code === 'E_NO_GEOCODER') { if (!googleApiKey) { throw new CodedError(error.code, `${error.message} Please set a Google API Key to use geocoding.`); } return _googleReverseGeocodeAsync(location); } throw error; }); } export function setApiKey(apiKey) { googleApiKey = apiKey; } async function _googleGeocodeAsync(address) { const result = await fetch(`${googleApiUrl}?key=${googleApiKey}&address=${encodeURI(address)}`); const resultObject = await result.json(); if (resultObject.status === 'ZERO_RESULTS') { return []; } assertGeocodeResults(resultObject); return resultObject.results.map(result => { let location = result.geometry.location; // TODO: This is missing a lot of props return { latitude: location.lat, longitude: location.lng, }; }); } async function _googleReverseGeocodeAsync(options) { const result = await fetch(`${googleApiUrl}?key=${googleApiKey}&latlng=${options.latitude},${options.longitude}`); const resultObject = await result.json(); if (resultObject.status === 'ZERO_RESULTS') { return []; } assertGeocodeResults(resultObject); return resultObject.results.map(result => { const address = {}; result.address_components.forEach(component => { if (component.types.includes('locality')) { address.city = component.long_name; } else if (component.types.includes('street_address')) { address.street = component.long_name; } else if (component.types.includes('administrative_area_level_1')) { address.region = component.long_name; } else if (component.types.includes('country')) { address.country = component.long_name; } else if (component.types.includes('postal_code')) { address.postalCode = component.long_name; } else if (component.types.includes('point_of_interest')) { address.name = component.long_name; } }); return address; }); } // https://developers.google.com/maps/documentation/geocoding/intro function assertGeocodeResults(resultObject) { const { status, error_message } = resultObject; if (status !== 'ZERO_RESULTS' && status !== 'OK') { if (error_message) { throw new CodedError(status, error_message); } else if (status === 'UNKNOWN_ERROR') { throw new CodedError(status, 'the request could not be processed due to a server error. The request may succeed if you try again.'); } throw new CodedError(status, `An error occurred during geocoding.`); } } // Polyfill: navigator.geolocation.watchPosition function watchPosition(success, error, options) { _maybeInitializeEmitterSubscription(); const watchId = _getNextWatchId(); watchCallbacks[watchId] = success; ExpoLocation.watchPositionImplAsync(watchId, options).catch(err => { _removeWatcher(watchId); error({ watchId, message: err.message, code: err.code }); }); return watchId; } export async function watchPositionAsync(options, callback) { _maybeInitializeEmitterSubscription(); const watchId = _getNextWatchId(); watchCallbacks[watchId] = callback; await ExpoLocation.watchPositionImplAsync(watchId, options); return { remove() { _removeWatcher(watchId); }, }; } // Polyfill: navigator.geolocation.clearWatch function clearWatch(watchId) { _removeWatcher(watchId); } function _removeWatcher(watchId) { // Do nothing if we have already removed the subscription if (!watchCallbacks[watchId]) { return; } ExpoLocation.removeWatchAsync(watchId); delete watchCallbacks[watchId]; if (Object.keys(watchCallbacks).length === 0 && deviceEventSubscription) { LocationEventEmitter.removeSubscription(deviceEventSubscription); deviceEventSubscription = null; } } function getCurrentPosition(success, error = () => { }, options = {}) { invariant(typeof success === 'function', 'Must provide a valid success callback.'); invariant(typeof options === 'object', 'options must be an object.'); _getCurrentPositionAsyncWrapper(success, error, options); } // This function exists to let us continue to return undefined from getCurrentPosition, while still // using async/await for the internal implementation of it async function _getCurrentPositionAsyncWrapper(success, error, options) { try { await ExpoLocation.requestPermissionsAsync(); const result = await getCurrentPositionAsync(options); success(result); } catch (e) { error(e); } } export async function requestPermissionsAsync() { await ExpoLocation.requestPermissionsAsync(); } // --- Location service export async function hasServicesEnabledAsync() { return await ExpoLocation.hasServicesEnabledAsync(); } // --- Background location updates function _validateTaskName(taskName) { invariant(taskName && typeof taskName === 'string', '`taskName` must be a non-empty string.'); } export async function isBackgroundLocationAvailableAsync() { const providerStatus = await getProviderStatusAsync(); return providerStatus.backgroundModeEnabled; } export async function startLocationUpdatesAsync(taskName, options = { accuracy: LocationAccuracy.Balanced }) { _validateTaskName(taskName); await ExpoLocation.startLocationUpdatesAsync(taskName, options); } export async function stopLocationUpdatesAsync(taskName) { _validateTaskName(taskName); await ExpoLocation.stopLocationUpdatesAsync(taskName); } export async function hasStartedLocationUpdatesAsync(taskName) { _validateTaskName(taskName); return ExpoLocation.hasStartedLocationUpdatesAsync(taskName); } // --- Geofencing function _validateRegions(regions) { if (!regions || regions.length === 0) { throw new Error('Regions array cannot be empty. Use `stopGeofencingAsync` if you want to stop geofencing all regions'); } for (const region of regions) { if (typeof region.latitude !== 'number') { throw new TypeError(`Region's latitude must be a number. Got '${region.latitude}' instead.`); } if (typeof region.longitude !== 'number') { throw new TypeError(`Region's longitude must be a number. Got '${region.longitude}' instead.`); } if (typeof region.radius !== 'number') { throw new TypeError(`Region's radius must be a number. Got '${region.radius}' instead.`); } } } export async function startGeofencingAsync(taskName, regions = []) { _validateTaskName(taskName); _validateRegions(regions); await ExpoLocation.startGeofencingAsync(taskName, { regions }); } export async function stopGeofencingAsync(taskName) { _validateTaskName(taskName); await ExpoLocation.stopGeofencingAsync(taskName); } export async function hasStartedGeofencingAsync(taskName) { _validateTaskName(taskName); return ExpoLocation.hasStartedGeofencingAsync(taskName); } export function installWebGeolocationPolyfill() { if (Platform.OS !== 'web') { // Polyfill navigator.geolocation for interop with the core react-native and web API approach to // geolocation // @ts-ignore window.navigator.geolocation = { getCurrentPosition, watchPosition, clearWatch, // We don't polyfill stopObserving, this is an internal method that probably should not even exist // in react-native docs stopObserving: () => { }, }; } } export { // For internal purposes LocationEventEmitter as EventEmitter, _getCurrentWatchId, }; //# sourceMappingURL=Location.js.map