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