import { Injectable } from '@angular/core'; import { BackgroundGeolocation, BackgroundGeolocationEvents } from '@ionic-native/background-geolocation/ngx'; import { BackgroundGeolocationConfig, BackgroundGeolocationResponse } from '@ionic-native/background-geolocation/ngx'; import { UserLocalNotificationService } from '../user-local-notification/user-local-notification.service'; import { Platform } from '@ionic/angular'; import { LocationModel } from './models/location.model'; import { ConnectorService } from '../connector/connector.service'; import { NativeStorage } from '@ionic-native/native-storage/ngx'; import { AuthenticationService } from '../authentication/authentication.service'; import { ZoneModel } from './models/zone.model'; import { CommonService } from '../common/common.service'; import { InsertLocationRequest } from './models/insert-location.request'; import { GeoUserModel } from './models/geo-user.model'; import { TranslatorService } from '../translator/translator.service'; export enum zoneState { out = 0, enter = 1, exit = 2, in = 3 } @Injectable({ providedIn: 'root' }) export class GeofencingService { public static ZONES_KEY = 'geofencing-zones'; public static USER_DATA_KEY = 'geofencing-user-data'; public static GEO_STATUS_KEY = 'geofencing-status'; public static zonesUrl = 'Services/Patients.svc/REST/GeoF_GetAllPoints'; public static loggingUrl = 'Services/Patients.svc/rest/GeoF_InsertPatientFileInfo'; private zonesList: ZoneModel []; private userData: GeoUserModel; private isPaused: boolean; private debug: boolean; locations: any []; constructor( private backgroundGeolocation: BackgroundGeolocation, public notif: UserLocalNotificationService, public platform: Platform, public con: ConnectorService, public auth: AuthenticationService, public storage: NativeStorage, public cs: CommonService, public authService: AuthenticationService, public ts: TranslatorService, ) { this.debug = false; } /* * logs location on server. */ private logLocation(id: number, type: number, userData: GeoUserModel, zone: ZoneModel) { this.toast('logging location..'); this.setNotification('Logging location ' , 'location id: ' + id + ' type: ' + type); const req = new InsertLocationRequest(); req.PointsID = id; req.GeoType = type; //req.PatientID = userData.PatientID; //req.PatientOutSA = userData.PatientOutSA; this.auth.setPublicFields(req); this.con.postNoLoad(GeofencingService.loggingUrl, req, () => { zone.pendingRequest = type; // failed to log, setup pending request this.storeZones(this.zonesList); this.log('pending request from onError'); this.setNotification('Pending request from onError', 'zone: ' + zone.Description + ' request type: ' + type); }).subscribe((response) => { if (this.cs.validResponse(response)) { this.setNotification('Log success' , 'location id: ' + id + ' patientID: ' + userData.PatientID); this.log('logged successfuly'); zone.pendingRequest = zoneState.out; // cancel pending requests if any } else { zone.pendingRequest = type; // failed to log, setup pending request this.log('pending request from invalid'); this.setNotification('Pending request from invalid', 'zone: ' + zone.Description + ' request type: ' + type); } this.storeZones(this.zonesList); }); } /* * Requests geofence zones list */ private requestZones() { const req = this.auth.getPublicRequest(); this.con.postNoLoad(GeofencingService.zonesUrl, req, _ => { // TODO: handle error }).subscribe((response: any) => { if (this.cs.validResponse(response)) { if (this.cs.hasData(response.GeoF_PointsList)) { // update zonelist if it doesnt exist or if the server has an updated one if (!this.cs.hasData(this.zonesList) || (this.zonesList.length !== response.GeoF_PointsList.length)) { this.log('setting new zone list'); this.zonesList = response.GeoF_PointsList; for (const zone of this.zonesList) { zone.in = false; } this.log('storing zones'); this.storeZones(this.zonesList); } } } }); } /* * Store zones list in local storage */ public async storeZones(zones: ZoneModel []) { return await this.storage.setItem(GeofencingService.ZONES_KEY, zones); } /* * Retrieve and set list of zones from local storage */ public async setZones() { try { const data = await this.storage.getItem(GeofencingService.ZONES_KEY); if (this.cs.hasData(data)) { this.zonesList = data; } else { this.requestZones(); // zones list not stored in local storage } } catch { this.requestZones(); // zones list not stored in local storage } } public async storeUserData(user: GeoUserModel) { return await this.storage.setItem(GeofencingService.USER_DATA_KEY, user); } /* * Retrieve and set user data from local storage */ public async setUserFromStorage() { const data = await this.storage.getItem(GeofencingService.USER_DATA_KEY); if (data) { this.userData = data; } else { this.userData = null; } } /* // turns location tracking on. Sets geo user data and zones list */ public async turnTrackingOn(patientID?: number, outSa?: number): Promise { this.toast('turning tracking on'); // set zones list from storage await this.setZones(); // update zone list from server this.requestZones(); // Set user data if (patientID) { this.userData = new GeoUserModel; this.userData.PatientID = patientID; this.userData.PatientOutSA = outSa; } else if (!this.userData) { try { await this.setUserFromStorage(); } catch { return false; } } this.userData.isTrackingOn = true; this.storeUserData(this.userData); this.startTracking(); return true; } /* // turns location tracking off. Stops background tracking */ public turnTrackingOff() { this.toast('turning tracking off'); this.stopTracking(); if (this.userData) { this.userData.isTrackingOn = false; this.storeUserData(this.userData); } else { this.setUserFromStorage().then(() => { this.userData.isTrackingOn = false; this.storeUserData(this.userData); }); } } /* // Check if geofencing service is enabled */ public async isTrackingEnabled(): Promise { // if (this.userData) { // return this.userData.isTrackingOn; // } else { try { await this.setUserFromStorage(); if (this.userData) { return this.userData.isTrackingOn; } else { return false; } } catch { throw false; } // } } /* // setup and start background gelocation plugin */ public startTracking() { const config: BackgroundGeolocationConfig = { desiredAccuracy: 10000, // HIGH_ACCURACY = 1 // low = 10000 stationaryRadius: 20, // 20 distanceFilter: 10, // 10 debug: false, // enable this hear sounds for background-geolocation life-cycle. stopOnTerminate: false, // enable this to clear background location settings when the app terminates // startOnBoot: true, // Android options notificationsEnabled: false, interval: 60000, // interval vars for improved battery life fastestInterval: 120000, activitiesInterval: 10000, startForeground: false, notificationTitle: 'Running in the background', notificationText: '', // IOS options saveBatteryOnBackground: false }; this.backgroundGeolocation.configure(config) .then((location: BackgroundGeolocationResponse) => { }); this.backgroundGeolocation.on(BackgroundGeolocationEvents.location) .subscribe((location: BackgroundGeolocationResponse) => { this.reportLocation(location); }); this.backgroundGeolocation.on(BackgroundGeolocationEvents.stationary) .subscribe((location: BackgroundGeolocationResponse) => { this.reportLocation(location); }); this.storage.getItem(GeofencingService.GEO_STATUS_KEY).then(status => { if (!status) { this.backgroundGeolocation.start(); this.storage.setItem(GeofencingService.GEO_STATUS_KEY, true); } }).catch(() => { this.toast('turning geofencing on'); this.backgroundGeolocation.start(); this.storage.setItem(GeofencingService.GEO_STATUS_KEY, true); }); } /* // Helper to startTracking() */ private async reportLocation(location: BackgroundGeolocationResponse) { // Check if zones and user is set if (!this.userData) { await this.setUserFromStorage(); } if (!this.cs.hasData(this.zonesList)) { await this.setZones(); } if (this.platform.is('ios')) { this.backgroundGeolocation.startTask().then((taskKey) => { this.checklocationInZones(location, this.zonesList, this.userData); this.backgroundGeolocation.endTask(taskKey); }); } else { this.checklocationInZones(location, this.zonesList, this.userData); } this.storeZones(this.zonesList); } /* // stops background geoloacation plugin tracking */ public stopTracking() { this.toast('Stop Tracking' ); this.storage.setItem(GeofencingService.GEO_STATUS_KEY, false); this.backgroundGeolocation.stop().then(value => { // this.toast('Stop Tracking value returned: ' + value); }); } /* // Checks if given location triggers any zone events (enter, exit) for all zones, and pauses the service if // the location was far from all zones */ private checklocationInZones(location: BackgroundGeolocationResponse, zones: ZoneModel [], user: GeoUserModel) { this.printLocation(location, 'check location in zones'); let currentMinDist = 9999999; for (const zone of zones) { const userLocation = new LocationModel(location.latitude, location.longitude); const event = this.checkLocationInZone(userLocation, zone); const currentDistance = this.distanceFromZone(userLocation, zone); currentMinDist = (currentMinDist > currentDistance ? currentDistance : currentMinDist); if (zone.GEOF_ID === 16) { this.log(zone.Description); this.log('zone state: ' + zone.in + ' pending: ' + zone.pendingRequest + 'current event: ' + event); } if (event === zoneState.in) { zone.in = true; } // send enter request if (event === zoneState.enter || (event === zoneState.in && zone.pendingRequest === zoneState.enter)) { zone.in = true; this.setNotification('Enter ZONE ' + zone.Description, 'location lat: ' + location.latitude + ' lng: ' + location.longitude); this.log('enter Zone' + zone.Description + ' ' + 'location: ' + location.latitude + ',' + location.longitude); this.logLocation(zone.GEOF_ID, zoneState.enter, user, zone); } // send exit request if (event === zoneState.exit || (event === zoneState.out && zone.pendingRequest === zoneState.exit)) { zone.in = false; this.setNotification('Exit ZONE ' + zone.Description, 'location lat: ' + location.latitude + ' lng: ' + location.longitude); this.log('exit Zone' + zone.Description + ' ' + 'location: ' + location.latitude + ',' + location.longitude); this.logLocation(zone.GEOF_ID, zoneState.exit, user, zone); } } const minDist = this.toDecimalDegrees(5000); // minimum distance location needs to be to all zones to pause service const maxDist = this.toDecimalDegrees(10000); // Max distance used to have a factor between 0-1 if (currentMinDist >= minDist) { const currentDis = (currentMinDist > maxDist ? maxDist : currentMinDist); this.pauseService((currentDis - minDist) / (maxDist - minDist)); } } /* // Checks the state of given location for the given zone */ private checkLocationInZone(userLocation: LocationModel, zone: ZoneModel): zoneState { const isIn = this.isPointInZone(userLocation.lng, userLocation.lat, zone.Longitude, zone.Latitude, this.toDecimalDegrees(zone.Radius)); if (isIn === false && zone.in === true) { return zoneState.exit; } if (isIn === true && zone.in === false) { return zoneState.enter; } if (isIn) { return zoneState.in; } return zoneState.out; } /* // calculates the distance between given location and given zone */ private distanceFromZone(userLocation: LocationModel, zone: ZoneModel): number { return this.distance(userLocation.lng, userLocation.lat, zone.Longitude, zone.Latitude); } /* // checks if a point p is in a circular zone with center z and radius r */ public isPointInZone(xp, yp, xz, yz, r): boolean { return this.distance(xp, yp, xz, yz) < r; } /* // Measures distance between two points */ public distance(xp, yp, xz, yz): number { return Math.sqrt(Math.pow(Math.abs(xp - xz), 2) + Math.pow(Math.abs(yp - yz) , 2)); } /* // Convert meters to decimal degrees */ public toDecimalDegrees(meters: number) { return meters / 1000 / 111.32; } /* // Pauses tracking service for a time multiplied by given factor */ public pauseService(factor: number) { if (!this.isPaused) { this.stopTracking(); this.isPaused = true; // time to pause min=120 sec max=1800 sec const time = (1680000 * factor); // time in milliseconds this.setNotification('pausing tracking service', 'Pause for ' + time + ' FYI factor is: ' + factor); setTimeout(() => { this.startTracking(); this.isPaused = false; this.setNotification('resuming tracking service', 'Paused for ' + time + ' FYI factor is: ' + factor); }, time); } } public startGeoFromHome() { if (this.cs.isCordova()) { this.isTrackingEnabled().then((result) => { if (result) { this.turnTrackingOff(); this.turnTrackingOn(); } }).catch(() => { // first time login, setup geofencing // local storage should reset when user logs out if (this.authService.isAuthenticated()) { const user = this.authService.getAuthenticatedUser(); this.turnTrackingOff(); this.cs.presentConfirmDialog(this.ts.trPK('settings', 'enable-geo'), () => { // this.turnTrackingOn(user.PatientID, +user.PatientOutSA); }, () => { // this.turnTrackingOn(user.PatientID, +user.PatientOutSA); this.turnTrackingOff(); }); } }); } } // ---------- TESTING FUNCTIONS -------------- // public setNotification(title: string, text: string) { if (this.debug) { const date = new Date(); date.setSeconds(date.getSeconds() + 2); this.notif.createNotification(title, text, date, 'geofencing'); } } public printLocation(location: BackgroundGeolocationResponse, source: string) { if (this.debug) { console.log('-------------------------------------------------------------'); console.log(source); console.log(this.zonesList); console.log(location); console.log('location: ' + location.latitude + ', ' + location.longitude); } } public printAllLocations() { if (this.debug) { this.backgroundGeolocation.getLocations().then(locations => { console.log(locations); }); } } public log(msg: string) { if (this.debug) { console.log(msg); } } public toast(msg: string) { if (this.debug) { this.cs.toast(msg); } } }