Commit 7bf42cf4 authored by Janik Münzenberger's avatar Janik Münzenberger
Browse files

Added push notification functionality

parent 1919327e
......@@ -9,13 +9,12 @@ import {NotificationService} from './services/notification.service';
styleUrls: ['./app.component.css'],
})
export class AppComponent {
readonly VAPID_PUBLIC_KEY = 'BNaGf2POUom9qpnI45OSE8gmzjeqdvk-4HoV7Is-3RjPeCWMtgcukwEVPp0K2xMdfmSrGCS2be5rFIYX2qRwoEc';
constructor(translate: TranslateService,
private notificationService: NotificationService) {
private notificationService: NotificationService) {
translate.setDefaultLang('en');
translate.use('en');
// this.subscribeToNotifications();
this.notificationService.subscribeToNotifications();
}
}
......@@ -17,17 +17,3 @@ label {
cursor: pointer;
}
.cover {
z-index: 999;
width: 100%;
height: 100%;
display: flex;
position: fixed;
top: 0;
left: 0;
align-items: center;
flex-direction: column;
justify-content: center;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
}
\ No newline at end of file
......@@ -6,12 +6,16 @@
<form #analysisForm="ngForm" class="row col-md-6 col-sm-12 mx-auto justify-content-center flex-column" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="crop_id">{{"ANALYSIS.SELECT_A_CROP" | translate}}</label>
<select id="crop_id" name="crop_id" class="custom-select mb-3" [(ngModel)]="selected">
<select id="crop_id" name="crop_id" class="custom-select mb-3" [(ngModel)]="selected" [disabled]="push">
<option *ngFor="let plant of plants" [value]="plant._id" >{{plant.name}}</option>
</select>
</div>
<div class="custom-file mb-3">
<input autocomplete="photo" type='file' (change)="onSelectFile($event)" accept="image/*" class="inputfile col-md-3" id="image_file" name="image_file"
<input autocomplete="photo" type='file'
(change)="onSelectFile($event)" accept="image/*"
class="inputfile col-md-3"
id="image_file" name="image_file"
[disabled]="push"
#image ngModel>
<label class="custom-file-label" for="image_file">{{"ANALYSIS.SELECT_IMAGE" | translate}}</label>
</div>
......@@ -21,16 +25,17 @@
<div *ngIf="showImage == true" class="form-group mt-2">
<label for="notification_email" class="col-form-label">{{"ANALYSIS.TO_EMAIL" | translate}}:</label>
<input type="email" autocomplete="email" id="notification_email" name="notification_email" class="form-control" [(ngModel)]="notification_email">
<input type="email" autocomplete="email"
id="notification_email" name="notification_email"
class="form-control" [(ngModel)]="notification_email"
[disabled]="push">
</div>
<div *ngIf="showImage == true" class="mt-3 mx-auto">
<div *ngIf="showImage == true" class="mt-3 mx-auto" [hidden]="push">
<button type="button" type="submit" class="btn btn-primary btn-lg">{{"ANALYSIS.ANALYSE" | translate}}</button>
</div>
</form>
<div class="col-md-8 alert alert-danger mx-auto" style="display: none;" #error>
{{"ANALYSIS.NO_RESULT" | translate}}
</div>
<div class="cover" #loader>
<div class="loader"></div>
<h3 class="mx-4 text-center">{{"ANALYSIS.PROGRESS" | translate}}</h3>
</div>
<div class="col-md-8 alert alert-success mx-auto" [hidden]="!push"><i class="fas fa-check mr-3"></i>{{"ANALYSIS.PUSH" | translate}}</div>
......@@ -6,6 +6,7 @@ import { AnalysisService } from '../../services/analysis.service';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
import { NotificationService } from '../../services/notification.service';
@Component({
selector: 'app-analyse',
......@@ -16,6 +17,8 @@ import { UserService } from '../../services/user.service';
export class AnalyseComponent implements OnInit {
url = '';
showImage = false;
push: boolean = false;
notification_email: string = "";
plants: IPlant[] = [];
......@@ -23,12 +26,12 @@ export class AnalyseComponent implements OnInit {
@ViewChild(NgForm) analysisForm:NgForm;
@ViewChild('image') image;
@ViewChild('loader') loader;
@ViewChild('error') error:ElementRef;
constructor(private pService: PlantService,
private aService: AnalysisService,
public userService: UserService,
private notificationService: NotificationService,
private router: Router,
private route: ActivatedRoute) {
......@@ -36,7 +39,6 @@ export class AnalyseComponent implements OnInit {
ngOnInit() {
if(this.userService.getUser()) this.notification_email = this.userService.getUser().email;
this.stopAnalysis();
this.getPlants();
}
......@@ -51,25 +53,25 @@ export class AnalyseComponent implements OnInit {
});
}
startAnalysis(){this.loader.nativeElement.style.display = 'flex';}
stopAnalysis(){this.loader.nativeElement.style.display = 'none';}
onSubmit(){
let element = this.image.nativeElement;
if(this.selected && element.files && element.files[0]){
this.startAnalysis();
const formData = new FormData();
formData.append('image_file', element.files[0]);
formData.append('crop_id', this.selected.toString());
formData.append('notification_email', this.notification_email);
if(this.notificationService.subscription)
formData.append('subscription', JSON.stringify(this.notificationService.subscription));
this.aService.startAnalysis(formData)
.then(res => {
this.stopAnalysis();
this.router.navigate(["result", res.data]);
if(res.method === 'poll'){
this.router.navigate(["result", res.data]);
}else{
this.push = true;
}
})
.catch(err => {
this.stopAnalysis();
this.error.nativeElement.style.display = 'block';
console.error(err);
});
......
......@@ -7,6 +7,20 @@
max-width: 85%;
}
.cover {
z-index: 999;
width: 100%;
height: 100%;
display: flex;
position: fixed;
top: 0;
left: 0;
align-items: center;
flex-direction: column;
justify-content: center;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
}
button {
height: auto;
......
......@@ -5,7 +5,7 @@
<div class="col-md-6">
<small>Job Id: {{job?._id}}</small>
<button type="button" class="btn btn-primary my-3" data-toggle="modal" data-target="#emailModal">
<i class="fas fa-envelope mr-2"></i>{{"RESULT.EMAIL" | translate}}
<i class="fas fa-envelope mr-2"></i>{{"RESULT.EMAIL" | translate}}
</button>
<h3>{{"RESULT.DISEASES" | translate}}</h3>
<div class="list-group">
......@@ -13,31 +13,32 @@
[routerLink]="['/disease', result?.disease_id?._id]">{{result?.disease_id?.name || ("RESULT.NONE" | translate)}}
<span class="badge badge-primary badge-pill">{{result?.confidence * 100 | number: '1.2'}}%</span>
</li>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" role="dialog" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="cover" tabindex="1" #loader>
<div class="loader"></div>
<h3 class="mx-4 text-center">{{"ANALYSIS.PROGRESS" | translate}}</h3>
</div>
<!-- Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" role="dialog" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">{{"RESULT.EMAIL" | translate}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-email #email [receivers]="gardeners" [message]="job?.result | stringify"></app-email>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" (click)="sendEmail()">Send</button>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">{{"RESULT.EMAIL" | translate}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-email #email [receivers]="gardeners" [message]="job?.result | stringify"></app-email>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" (click)="sendEmail()">Send</button>
</div>
</div>
</div>
</div>
</div>
</div>
import {Component, OnInit, ViewChild} from '@angular/core';
import {Router, ActivatedRoute, Params} from '@angular/router';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { AnalysisService } from '../../services/analysis.service';
import { IJob } from '../../model/IJob';
import { Subscription } from 'rxjs';
......@@ -18,16 +18,19 @@ export class ResultComponent implements OnInit {
job: IJob;
sub: Subscription;
apiHost: string = environment.API_HOST;
gardeners: IGardener[] = [];
gardeners: IGardener[] = [];
@ViewChild('loader') loader;
@ViewChild("email") emailModal: EmailComponent;
constructor(private router: Router,
private route: ActivatedRoute,
constructor(private router: Router,
private route: ActivatedRoute,
private aService: AnalysisService,
private gService: GardenerService) { }
ngOnInit() {
this.stopAnalysis();
this.sub = this.route.params.subscribe((params: Params) => {
let id = params['id'];
this.getResult(id);
......@@ -35,21 +38,32 @@ export class ResultComponent implements OnInit {
this.getGardeners();
}
getResult(id:string){
this.aService.getResult(id)
.then(job => {
this.job = job;
})
.catch(err => console.log(err));
startAnalysis() { this.loader.nativeElement.style.display = 'flex'; }
stopAnalysis() { this.loader.nativeElement.style.display = 'none'; }
getResult(id: string) {
let interval = setInterval(() => {
this.aService.getResult(id)
.then(res => {
if(res.success){
this.job = res.data;
this.stopAnalysis();
clearInterval(interval);
}else{
this.startAnalysis();
}
})
.catch(err => console.error(err));
}, 1000);
}
getGardeners(){
getGardeners() {
this.gService.getAllGardeners()
.then(gardeners => this.gardeners = gardeners)
.catch(err => console.error("Fehler"));
}
sendEmail(){
sendEmail() {
this.emailModal.send();
}
}
......@@ -10,23 +10,11 @@ export class AnalysisService{
constructor(private apiService: ApiService) { }
startAnalysis(data: FormData): Promise<any> {
return new Promise((resolve, reject) => {
this.apiService.post('analysis', data)
.subscribe(
(res: any) => resolve(res),
(err: any) => reject(err)
)
});
return this.apiService.post('analysis', data).toPromise();
}
getResult(id: string): Promise<IJob>{
return new Promise<IJob>((resolve, reject) => {
this.apiService.get('result/' + id)
.subscribe(
(res: any) => resolve(res.data),
(err: any) => reject(err)
)
});
getResult(id: string): Promise<any>{
return this.apiService.get('result/' + id).toPromise();
}
getHistory(){
......
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import { SwPush } from '@angular/service-worker';
import { Router } from '@angular/router';
@Injectable()
export class NotificationService {
constructor(private http: HttpClient) {
readonly VAPID_PUBLIC_KEY = 'BNaGf2POUom9qpnI45OSE8gmzjeqdvk-4HoV7Is-3RjPeCWMtgcukwEVPp0K2xMdfmSrGCS2be5rFIYX2qRwoEc';
subscription: PushSubscription;
}
constructor(private swPush: SwPush, private router: Router) {
addEventListener('notificationclick', (event: any) => {
console.log("cllicked on Notification");
if(event.notification.data.jobId){
router.navigate(['/result', event.notification.data.jobId]);
}else{
console.log("Test");
}
});
}
addPushSubscriber(sub: any) {
return this.http.post('/api/notification', sub);
subscribeToNotifications(){
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
}).then(sub => {
this.subscription = sub;
console.log(this.subscription);
console.log(JSON.stringify(sub));
})
.catch(err => console.error(err));
}
}
......@@ -81,6 +81,7 @@
"SELECT_IMAGE": "Foto auswählen / machen",
"TO_EMAIL": "Sende Ergbnis an meine Email",
"ANALYSE": "Analysieren",
"PUSH": "Analyse erfolgreich gestartet. Sie werden benachrichtigt sobald das Ergebnis fertig ist.",
"PROGRESS": "Ihr Foto wird analysiert. Bitte warten.",
"NO_RESULT": "Tut uns leid, aber wir konnten kein Ergebnis für ihre Analyse erhalten. Bitte versuchen sie es später noch einmal"
},
......
......@@ -82,7 +82,8 @@
"TO_EMAIL": "Send the result to my email",
"ANALYSE": "Analyse",
"NO_RESULT": "Sorry we couldnt get a result for your image in time. Please try again later.",
"PROGRESS": "Your Analysis is progressing. Please wait."
"PROGRESS": "Waiting for analysis result.",
"PUSH": "Successfully startet the analysis. You will be notified when the result is ready."
},
"PROFILE": {
"INFORMATION": "Information",
......
......@@ -10,58 +10,53 @@ const router = require('express').Router(),
randomstring = require('randomstring');
// Request from the User to our Server
function analysis(req, res) {
async function analysis(req, res) {
let cropId = req.body.crop_id;
const subscription = req.body.subscription;
console.log(subscription);
let jobImage = req.files.image_file;
let extension = req.files.image_file.name.split('.')[1];
let filename = `${randomstring.generate()}.${extension}`;
let relImagePath = '../assets/analysis/' + filename;
let imagePath = path.resolve(__dirname, relImagePath);
moveFile(jobImage, imagePath)
.then(path => {
const options = getOptionsFromRequest(req, path, filename);
getRequestId(options)
.then(request_id => {
winston.info(`result request_id ${request_id}`);
addJob(filename, cropId, request_id, req.tokenData)
.then(jobId => {
let counter = config.api.reload_counter;
let sent = false;
let interval = setInterval(() => {
getResults(request_id)
.then(data => {
if (counter < 0) {
winston.error(`Could not get result ${request_id} in time.`);
clearInterval(interval);
throw new errors.ResultTimeOutError('Result could not be fetched in time!');
} else if (data.statusCode === 204) {
winston.info(`Wait for result ${request_id}`);
counter--;
} else {
sent = true;
res.status(200);
res.send({
success: true,
data: jobId
});
completeJob(jobId, data.body);
clearInterval(interval);
}
}).catch(err => {
winston.error(err);
errors.sendError(res, err, 500);
});
}, config.api.reload_timer);
});
try {
await moveFile(jobImage, imagePath);
const options = getOptionsFromRequest(req, imagePath, filename);
const request_id = (await rp(options)).request_id;
winston.info(`result request_id ${request_id}`);
const jobId = await addJob(filename, cropId, request_id, req.tokenData, subscription);
if (subscription) {
res.send({
success: true,
method: 'push',
data: jobId
});
} else {
const response = await getResults(request_id);
if (response.statusCode === 204) {
winston.error(`Could not get result ${request_id}.`);
res.send({
success: false,
method: 'poll',
data: jobId
});
}).catch(err => {
winston.error(err);
errors.sendError(res, err, 500);
});
} else {
res.status(200);
res.send({
success: true,
method: 'poll',
data: jobId
});
completeJob(jobId, response.body);
}
}
} catch (err) {
winston.error(err);
errors.sendError(res, err, 500);
};
}
// Move imagefile from request into folder
......@@ -101,12 +96,6 @@ function getOptionsFromRequest(req, imagePath, imageName) {
};
}
// Get Request id Promise
function getRequestId(options) {
return rp(options)
.then(res => res.request_id);
}
// Get results by request id
function getResults(request_id) {
const options = {
......@@ -118,15 +107,16 @@ function getResults(request_id) {
json: true,
resolveWithFullResponse: true,
};
return rp(options).then(res => res);
return rp(options);
}
function addJob(imageName, plantJob, resultIdJob, user = null) {
function addJob(imageName, plantJob, resultIdJob, user = null, subscription = null) {
// new Job
let job = new Job({
image_url: '/uploads/analysis/' + imageName,
plant: plantJob,
resultId: resultIdJob
resultId: resultIdJob,
subscription: subscription
});
// new job in the DB
......@@ -170,41 +160,55 @@ function addToUser(userId, jobId) {
winston.warn("addToUser: " + err);
});
}
function getJob(req, res) {
/**
* Get the job by params id
*
* If job is already finished send to client
* Else try to get the result from the api
* If result is now available save results to object and return to client
* Else return TimeOutError
*
* @param {*} req Request
* @param {*} res Response
*/
async function getJob(req, res) {
let id = req.params.id;
try {
let job = await Job.findById(id).populate('result.disease_id', ['name', 'symptoms']).populate('plant', ['name', 'image_url']);
if (!job) throw errors.DBError('Job could not be found');
if (job.finish) {
res.send({
success: true,
data: job
});
} else {
const apiResponse = await getResults(job.resultId);
if (apiResponse.statusCode === 204) {
res.send({
success: false,
data: job._id
});
}
else {
job.result = apiResponse.body;
job.finish = true;
job = await job.save();
job = await Job.findById(job._id).populate('result.disease_id', ['name', 'symptoms']).populate('plant', ['name', 'image_url'])
Job.findById(id).populate('result.disease_id', ['name', 'symptoms']).populate('plant', ['name', 'image_url'])
.then(job => {
if (job.finish === true) {
res.send({
success: true,
data: job
});
} else {
getResults(job.resultId)
.then(response => {
if (response.statusCode === 204) throw new errors.TimeoutError('Results are stil not available');
else {
job.result = response.body;
job.finish = true;
job.save().then(job => {
Job.findById(job._id).populate('result.disease_id', ['name', 'symptoms']).populate('plant', ['name', 'image_url'])
.then(job => {
res.send({
success: true,
data: job