You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mohemm_moe/Mohem/src/app/hmg-common/services/geofencing/geofencing.service.ts

447 lines
16 KiB
TypeScript

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<boolean> {
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<boolean> {
// 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);
}
}
}