import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { later } from '@ember/runloop';
import { htmlSafe } from '@ember/template';
interface IGpsService {
inUse: boolean
allowedDistance: number
mapsLoaded: boolean
status: string
errorTooltipText: SafeString | null
lockAccuracy: number
lockedLatitude: string | null
lockedLongitude: string | null
lockedAccuracy: number | null
lockedPosition: IPosition | null
intl: any
start(): void
}
interface IPosition {
longitude: string,
latitude: string,
coords?: any
}
interface IGpsWindow {
google: any
}
declare const window: IGpsWindow
/**
* This service handles the application's gps.
*
* @class GpsService
* @public
*/
export default class GpsService extends Service implements IGpsService {
@service public intl!: any;
public allowedDistance: number = 700
public lockAccuracy: number = 200
public inUse: boolean = false
public status: string = 'unknown'
public errorTooltipText: SafeString | null = null
public lockedLatitude: string | null = null
public lockedLongitude: string | null = null
public lockedAccuracy: number | null = null
public notSupported: boolean = false
@tracked public lockedPosition: IPosition | null = null
@tracked public distanceToProject: number | null = null
protected currentLatitude: string | null = null
protected currentLongitude: string | null = null
protected currentAccuracy: number | null = null
private errorShown: boolean = false;
private _lastError: string | null = null;
/**
* Call this method to fetch the user's initlal location and to set up location watching.
* This method will check if GPS service is already in use so it will not start twice.
* Uses `navigator.geolocation`
*
* @method start
* @public
* @return {void}
*/
/* istanbul ignore next */
public start(): void {
if (this.inUse) return
this.inUse = true
later(this, () => {
if (!this.lockedPosition) {
this.lastError = 'CUSTOMTIMEOUT'
}
}, 8000)
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.setPosition.bind(this), this.positionError.bind(this))
navigator.geolocation.watchPosition(this.setPosition.bind(this), this.positionError.bind(this), {enableHighAccuracy : true})
} else {
this.notSupported = true
}
// this.allowedDistance = this.customerSettings.checkSetting('gps_distance') || 700
}
/**
* Get the distance between two `location: IPosition` objects.
* @method getDistance
* @param {IPosition} loc1
* @param {IPosition} loc2
* @public
* @return {Number}
*/
/* istanbul ignore next */
public getDistance(loc1: IPosition, loc2: IPosition): number {
const gloc1 = new window.google!.maps.LatLng(loc1.latitude, loc1.longitude)
const gloc2 = new window.google!.maps.LatLng(loc2.latitude, loc2.longitude)
return window.google!.maps.geometry.spherical.computeDistanceBetween(gloc1, gloc2)
}
/**
* Success Callback for navigator.geolocation
* Sets user's current location, and calls lockLocation to lock the user's current position.
* @method setPosition
* @param {IPosition} position
* @private
* @return {Void}
*/
private setPosition(position: IPosition): void {
this.lastError = null
if (this.status === 'error') {
this.status = 'searching'
}
this.currentAccuracy = position.coords.accuracy
this.currentLongitude = position.coords.longitude
this.currentLatitude = position.coords.latitude
this.lockLocation(position.coords.latitude, position.coords.longitude, position.coords.accuracy)
}
/**
* Error Callback for navigator.geolocation.
* Sets this service's last seen error message
* @method positionError
* @param {any} error
* @private
* @return {Void}
*/
private positionError(error: any) {
this.resetPosition()
if (error.code === error.PERMISSION_DENIED) this.setLastError('PERMISSION_DENIED')
else if (error.code === error.POSITION_UNAVAILABLE) this.setLastError('POSITION_UNAVAILABLE')
else if (error.code === error.TIMEOUT) this.setLastError ('TIMEOUT')
else this.setLastError('UNKNOWN_ERROR')
}
/**
* Called by `setPosition`, which is called by `navigator.geolocation.getCurrentPosition` and `navigator.geolocation.watchPosition`
*
* Sets the following public properties:
* ```
* this.lockedLatitude = lat
* this.lockedLongitude = lon
* this.lockedPosition = {latitude: lat, longitude: lon}
* this.lockedAccuracy = this.currentAccuracy
* ```
* @method lockLocation
* @param {String} lat
* @param {String} long
* @param {Number} accuracy
* @private
* @return {Void}
*/
private lockLocation(lat: string, lon: string, accuracy: number): void {
/* istanbul ignore next */
if (!this.mapsLoaded) {
later(this, () => {
this.lockLocation(lat, lon, accuracy)
}, 3000)
return
}
if (accuracy < this.lockAccuracy) {
if (this.checkLocationChange(lat, lon, 50)) {
this.lockedLatitude = lat
this.lockedLongitude = lon
this.lockedPosition = {latitude: lat, longitude: lon}
this.lockedAccuracy = this.currentAccuracy
}
this.status = 'located'
}
}
/**
* Checks if the gps location has changed enough.
* Calls `getDistance`, comparing the method's parameters to lockedLatitude and lockedLongitude
* returns true if the distance between is greater than the change param
* @method checkLocationChange
* @param {String} latitude
* @param {String} longitude
* @param {Number} change
* @private
* @return {Boolean}
*/
private checkLocationChange(latitude: string, longitude: string, change: number): boolean {
if (!this.lockedLatitude || !this.lockedLongitude) return true
const loc1 = {latitude: this.lockedLatitude, longitude: this.lockedLongitude}
const loc2 = {latitude, longitude}
const dist = this.getDistance(loc1, loc2)
return (dist > change);
}
/**
* Rests currentLatitude, currentLongitude, lockedLatitude, and lockedLongitude
* @method resetPosition
* @private
* @return {Void}
*/
private resetPosition(): void {
this.currentLatitude = this.currentLongitude = this.lockedLatitude = this.lockedLongitude = null
}
private setLastError(error: any) {
if (!this.errorShown) {
this.errorTooltipText = htmlSafe(this.intl.t('gps.errors.general_tooltip').replace(/\n/g, '
'))
this.errorShown = true
}
this.lastError = error
this.status = 'error'
}
get mapsLoaded(): boolean {
return window.google?.maps ? true : false
}
get lastError(): string | null {
return this._lastError
}
set lastError(error: string | null) {
this._lastError = error
}
}