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.
447 lines
16 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|