@@ -0,0 +1,16 @@ | |||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. | |||
# For additional information regarding the format and rule options, please see: | |||
# https://github.com/browserslist/browserslist#queries | |||
# For the full list of supported browsers by the Angular framework, please see: | |||
# https://angular.io/guide/browser-support | |||
# You can see what browsers were selected by your queries by running: | |||
# npx browserslist | |||
last 1 Chrome version | |||
last 1 Firefox version | |||
last 2 Edge major versions | |||
last 2 Safari major versions | |||
last 2 iOS major versions | |||
Firefox ESR |
@@ -0,0 +1,16 @@ | |||
# Editor configuration, see https://editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
indent_style = space | |||
indent_size = 2 | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.ts] | |||
quote_type = single | |||
[*.md] | |||
max_line_length = off | |||
trim_trailing_whitespace = false |
@@ -0,0 +1,42 @@ | |||
# See http://help.github.com/ignore-files/ for more about ignoring files. | |||
# Compiled output | |||
/dist | |||
/tmp | |||
/out-tsc | |||
/bazel-out | |||
# Node | |||
/node_modules | |||
npm-debug.log | |||
yarn-error.log | |||
# IDEs and editors | |||
.idea/ | |||
.project | |||
.classpath | |||
.c9/ | |||
*.launch | |||
.settings/ | |||
*.sublime-workspace | |||
# Visual Studio Code | |||
.vscode/* | |||
!.vscode/settings.json | |||
!.vscode/tasks.json | |||
!.vscode/launch.json | |||
!.vscode/extensions.json | |||
.history/* | |||
# Miscellaneous | |||
/.angular/cache | |||
.sass-cache/ | |||
/connect.lock | |||
/coverage | |||
/libpeerconnection.log | |||
testem.log | |||
/typings | |||
# System files | |||
.DS_Store | |||
Thumbs.db |
@@ -0,0 +1,27 @@ | |||
# Frontend | |||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.8. | |||
## Development server | |||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. | |||
## Code scaffolding | |||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. | |||
## Build | |||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. | |||
## Running unit tests | |||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). | |||
## Running end-to-end tests | |||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. | |||
## Further help | |||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. |
@@ -0,0 +1,105 @@ | |||
{ | |||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", | |||
"version": 1, | |||
"newProjectRoot": "projects", | |||
"projects": { | |||
"Frontend": { | |||
"projectType": "application", | |||
"schematics": {}, | |||
"root": "", | |||
"sourceRoot": "src", | |||
"prefix": "app", | |||
"architect": { | |||
"build": { | |||
"builder": "@angular-devkit/build-angular:browser", | |||
"options": { | |||
"outputPath": "dist/frontend", | |||
"index": "src/index.html", | |||
"main": "src/main.ts", | |||
"polyfills": "src/polyfills.ts", | |||
"tsConfig": "tsconfig.app.json", | |||
"assets": [ | |||
"src/favicon.ico", | |||
"src/assets" | |||
], | |||
"styles": [ | |||
"src/custom-theme.scss", | |||
"src/styles.css", | |||
"node_modules/leaflet/dist/leaflet.css" | |||
], | |||
"scripts": [] | |||
}, | |||
"configurations": { | |||
"production": { | |||
"budgets": [ | |||
{ | |||
"type": "initial", | |||
"maximumWarning": "500kb", | |||
"maximumError": "1mb" | |||
}, | |||
{ | |||
"type": "anyComponentStyle", | |||
"maximumWarning": "2kb", | |||
"maximumError": "4kb" | |||
} | |||
], | |||
"fileReplacements": [ | |||
{ | |||
"replace": "src/environments/environment.ts", | |||
"with": "src/environments/environment.prod.ts" | |||
} | |||
], | |||
"outputHashing": "all" | |||
}, | |||
"development": { | |||
"buildOptimizer": false, | |||
"optimization": false, | |||
"vendorChunk": true, | |||
"extractLicenses": false, | |||
"sourceMap": true, | |||
"namedChunks": true | |||
} | |||
}, | |||
"defaultConfiguration": "production" | |||
}, | |||
"serve": { | |||
"builder": "@angular-devkit/build-angular:dev-server", | |||
"configurations": { | |||
"production": { | |||
"browserTarget": "Frontend:build:production" | |||
}, | |||
"development": { | |||
"browserTarget": "Frontend:build:development" | |||
} | |||
}, | |||
"defaultConfiguration": "development" | |||
}, | |||
"extract-i18n": { | |||
"builder": "@angular-devkit/build-angular:extract-i18n", | |||
"options": { | |||
"browserTarget": "Frontend:build" | |||
} | |||
}, | |||
"test": { | |||
"builder": "@angular-devkit/build-angular:karma", | |||
"options": { | |||
"main": "src/test.ts", | |||
"polyfills": "src/polyfills.ts", | |||
"tsConfig": "tsconfig.spec.json", | |||
"karmaConfig": "karma.conf.js", | |||
"assets": [ | |||
"src/favicon.ico", | |||
"src/assets" | |||
], | |||
"styles": [ | |||
"src/custom-theme.scss", | |||
"src/styles.css", | |||
"node_modules/leaflet/dist/leaflet.css" | |||
], | |||
"scripts": [] | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
// Karma configuration file, see link for more information | |||
// https://karma-runner.github.io/1.0/config/configuration-file.html | |||
module.exports = function (config) { | |||
config.set({ | |||
basePath: '', | |||
frameworks: ['jasmine', '@angular-devkit/build-angular'], | |||
plugins: [ | |||
require('karma-jasmine'), | |||
require('karma-chrome-launcher'), | |||
require('karma-jasmine-html-reporter'), | |||
require('karma-coverage'), | |||
require('@angular-devkit/build-angular/plugins/karma') | |||
], | |||
client: { | |||
jasmine: { | |||
// you can add configuration options for Jasmine here | |||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html | |||
// for example, you can disable the random execution with `random: false` | |||
// or set a specific seed with `seed: 4321` | |||
}, | |||
clearContext: false // leave Jasmine Spec Runner output visible in browser | |||
}, | |||
jasmineHtmlReporter: { | |||
suppressAll: true // removes the duplicated traces | |||
}, | |||
coverageReporter: { | |||
dir: require('path').join(__dirname, './coverage/frontend'), | |||
subdir: '.', | |||
reporters: [ | |||
{ type: 'html' }, | |||
{ type: 'text-summary' } | |||
] | |||
}, | |||
reporters: ['progress', 'kjhtml'], | |||
port: 9876, | |||
colors: true, | |||
logLevel: config.LOG_INFO, | |||
autoWatch: true, | |||
browsers: ['Chrome'], | |||
singleRun: false, | |||
restartOnFileChange: true | |||
}); | |||
}; |
@@ -0,0 +1,50 @@ | |||
{ | |||
"name": "frontend", | |||
"version": "0.0.0", | |||
"scripts": { | |||
"ng": "ng", | |||
"start": "ng serve", | |||
"build": "ng build", | |||
"watch": "ng build --watch --configuration development", | |||
"test": "ng test" | |||
}, | |||
"private": true, | |||
"dependencies": { | |||
"@angular/animations": "^14.2.0", | |||
"@angular/cdk": "^13.0.0", | |||
"@angular/common": "^14.2.0", | |||
"@angular/compiler": "^14.2.0", | |||
"@angular/core": "^14.2.0", | |||
"@angular/forms": "^14.2.0", | |||
"@angular/material": "^13.0.0", | |||
"@angular/platform-browser": "^14.2.0", | |||
"@angular/platform-browser-dynamic": "^14.2.0", | |||
"@angular/router": "^14.2.0", | |||
"@types/googlemaps": "^3.43.3", | |||
"@types/leaflet": "^1.9.3", | |||
"leaflet": "^1.9.3", | |||
"mqtt": "^4.3.7", | |||
"ngx-mqtt": "^15.0.0-alpha.1", | |||
"rxjs": "~7.5.0", | |||
"tslib": "^2.3.0", | |||
"zone.js": "~0.11.4" | |||
}, | |||
"devDependencies": { | |||
"@angular-devkit/build-angular": "^14.2.8", | |||
"@angular/cli": "~14.2.8", | |||
"@angular/compiler-cli": "^14.2.0", | |||
"@types/jasmine": "~4.0.0", | |||
"jasmine-core": "~4.3.0", | |||
"karma": "~6.4.0", | |||
"karma-chrome-launcher": "~3.1.0", | |||
"karma-coverage": "~2.2.0", | |||
"karma-jasmine": "~5.1.0", | |||
"karma-jasmine-html-reporter": "~2.0.0", | |||
"typescript": "~4.7.2" | |||
}, | |||
"description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.8.", | |||
"main": "karma.conf.js", | |||
"keywords": [], | |||
"author": "", | |||
"license": "ISC" | |||
} |
@@ -0,0 +1 @@ | |||
Subproject commit a13b150dfe70b0beb1484f051ad4d7076ba7fd62 |
@@ -0,0 +1,55 @@ | |||
#robotIsOnTour { | |||
color: red; | |||
font-size: larger; | |||
} | |||
#robotIsReady { | |||
color: green; | |||
font-size: larger; | |||
} | |||
.container { | |||
display: grid; | |||
grid-template-rows: 40% 20% 7%; | |||
width: 100%; | |||
height: 100%; | |||
font-family: Cormorant; | |||
} | |||
.item-input { | |||
grid-row: 1 / 2; | |||
grid-column: 1/-1; | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
.item-box-input { | |||
width: 100%; | |||
} | |||
.item-box-output { | |||
width: 100%; | |||
} | |||
.item-output { | |||
grid-row: 3 / 4; | |||
grid-column: 1/-1; | |||
justify-self: start; | |||
align-self: start; | |||
} | |||
.item-robotText { | |||
grid-row: 2 / 3; | |||
grid-column: 1/-1; | |||
justify-self: center; | |||
align-self: center; | |||
font-size: 1.2rem; | |||
padding: 0.7rem; | |||
border: 0.125rem; | |||
border-style: solid; | |||
border-radius: 1rem; | |||
border-color: silver; | |||
box-shadow: 0.2rem 0.2rem 0.2rem silver; | |||
} |
@@ -0,0 +1,16 @@ | |||
<div class="container"> | |||
<div class="item-box-input"> | |||
<div class="item-input"> | |||
<app-input></app-input> | |||
</div> | |||
</div> | |||
<div class="item-box-output"> | |||
<div class="item-output"> | |||
<app-output></app-output> | |||
</div> | |||
</div> | |||
<div class="item-robotText"> | |||
<div *ngIf="robotStateChange$ | async" id="robotIsOnTour">Robot is on Tour</div> | |||
<div *ngIf="!(robotStateChange$ | async)" id="robotIsReady">Robot is Ready</div> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { HomeComponent } from './home.component'; | |||
// describe('HomeComponent', () => { | |||
// let component: HomeComponent; | |||
// let fixture: ComponentFixture<HomeComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ HomeComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(HomeComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,16 @@ | |||
import { Component } from '@angular/core'; | |||
import { Observable } from 'rxjs'; | |||
import { StoreService } from '../Service/store.service'; | |||
@Component({ | |||
selector: 'app-home', | |||
templateUrl: './home.component.html', | |||
styleUrls: ['./home.component.css'] | |||
}) | |||
export class HomeComponent { | |||
robotStateChange$: Observable<boolean> = this.storeService.currentRobotReady; | |||
constructor(private storeService: StoreService) { | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
.container { | |||
display: grid; | |||
width: 100%; | |||
height: 100%; | |||
grid-template-rows: 25% 75%; | |||
font-family: Cormorant; | |||
margin-top: 3rem; | |||
} | |||
.inputText { | |||
grid-row: 1 / 2; | |||
justify-self: center; | |||
align-self: center; | |||
font-size: 2rem; | |||
} | |||
.inputMicro { | |||
width: 8.3rem; | |||
height: 100%; | |||
grid-row: 2 / 3; | |||
justify-self: center; | |||
align-self: start; | |||
} | |||
button { | |||
padding: 6.25rem 2.31rem; | |||
border: 0.125rem; | |||
border-style: solid; | |||
border-radius: 10.625rem; | |||
border-color: silver; | |||
box-shadow: 0.2rem 0.2rem 0.2rem silver; | |||
background-color: white; | |||
} | |||
mat-icon { | |||
transform: scale(8); | |||
margin-left: 1rem; | |||
} |
@@ -0,0 +1,10 @@ | |||
<div class="container"> | |||
<div class="inputText"> | |||
<span>{{text}}</span> | |||
</div> | |||
<div class="inputMicro"> | |||
<button mat-menu-item (click)="startStopService()"> | |||
<mat-icon [ngStyle]="{'color': micIsClicked ? 'blue' : 'silver' }">micro</mat-icon> | |||
</button> | |||
</div> | |||
</div> |
@@ -0,0 +1,69 @@ | |||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { SpeechService } from '../speech.service'; | |||
import { InputComponent } from './input.component'; | |||
describe('InputComponent', () => { | |||
let component: InputComponent; | |||
let fixture: ComponentFixture<InputComponent>; | |||
let speechService: SpeechService; | |||
let mqttRequestService: MqttRequestService; | |||
beforeEach(async () => { | |||
await TestBed.configureTestingModule({ | |||
declarations: [InputComponent], | |||
providers: [SpeechService, MqttRequestService], | |||
}).compileComponents(); | |||
}); | |||
beforeEach(() => { | |||
fixture = TestBed.createComponent(InputComponent); | |||
component = fixture.componentInstance; | |||
speechService = TestBed.inject(SpeechService); | |||
mqttRequestService = TestBed.inject(MqttRequestService); | |||
fixture.detectChanges(); | |||
}); | |||
it('should create the InputComponent', () => { | |||
expect(component).toBeTruthy(); | |||
}); | |||
it('should initialize the SpeechService if not already initialized', () => { | |||
spyOn(speechService, 'init'); | |||
component.ngOnInit(); | |||
expect(speechService.init).toHaveBeenCalled(); | |||
}); | |||
it('should update the text and publish to INPUT when receiving a message', () => { | |||
const message = 'Test message'; | |||
spyOn(mqttRequestService, 'publishToINPUT'); | |||
component.ngOnInit(); | |||
// component.service.currentApprovalStageMessage.next(message); | |||
expect(component.text).toBe(message); | |||
expect(mqttRequestService.publishToINPUT).toHaveBeenCalledWith(message); | |||
}); | |||
it('should stop the service when micIsClicked is true', () => { | |||
spyOn(component.service, 'stop'); | |||
component.micIsClicked = true; | |||
component.startStopService(); | |||
expect(component.service.stop).toHaveBeenCalled(); | |||
}); | |||
it('should start the service when micIsClicked is false', () => { | |||
spyOn(component.service, 'start'); | |||
component.micIsClicked = false; | |||
component.startStopService(); | |||
expect(component.service.start).toHaveBeenCalled(); | |||
}); | |||
it('should unsubscribe from subscription and call onDestroy', () => { | |||
spyOn(component.subscription, 'unsubscribe'); | |||
spyOn(component.service, 'onDestory'); | |||
component.ngOnDestroy(); | |||
expect(component.subscription.unsubscribe).toHaveBeenCalled(); | |||
expect(component.service.onDestory).toHaveBeenCalled(); | |||
expect(component.text).toBe(''); | |||
}); | |||
}); | |||
@@ -0,0 +1,51 @@ | |||
import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
import { Subscription } from 'rxjs-compat'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { SpeechService } from '../speech.service'; | |||
@Component({ | |||
selector: 'app-input', | |||
templateUrl: './input.component.html', | |||
styleUrls: ['./input.component.css'] | |||
}) | |||
export class InputComponent implements OnInit, OnDestroy { | |||
text: string = 'Init'; | |||
micIsClicked = false; | |||
subscription: Subscription; | |||
constructor( | |||
public service: SpeechService, | |||
private mqttRequestService: MqttRequestService | |||
) { | |||
if (!this.service.initSpeechServiceStarted) { | |||
this.service.init(); | |||
} | |||
} | |||
ngOnInit(): void { | |||
this.subscription = this.service.currentApprovalStageMessage.subscribe(msg => { | |||
this.text = msg; | |||
if (this.text !== '') { | |||
this.micIsClicked = false; | |||
this.mqttRequestService.publishToINPUT(this.text); | |||
} | |||
}); | |||
} | |||
startStopService() { | |||
if (this.micIsClicked) { | |||
this.micIsClicked = false; | |||
this.service.stop(); | |||
} else { | |||
this.micIsClicked = true; | |||
this.service.start(); | |||
} | |||
} | |||
ngOnDestroy() { | |||
this.subscription.unsubscribe() | |||
this.service.onDestory(); | |||
this.text = ''; | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
.container { | |||
display: grid; | |||
grid-template-columns: 10% 90%; | |||
height: 100%; | |||
width: 100%; | |||
font-family: Cormorant; | |||
border: 0.125rem; | |||
border-style: solid; | |||
border-radius: 1rem; | |||
border-color: silver; | |||
box-shadow: 0.2rem 0.2rem 0.2rem silver; | |||
} | |||
.outputIcon { | |||
grid-column: 1 / 2; | |||
justify-self: start; | |||
align-self: end; | |||
} | |||
.outputText { | |||
grid-column: 2 / 3; | |||
justify-self: start; | |||
align-self: center; | |||
font-size: 2rem; | |||
margin: 0; | |||
} | |||
.mat-icon { | |||
margin-left: 0.5rem; | |||
margin-top: 0.5rem; | |||
transform: scale(1.5); | |||
} |
@@ -0,0 +1,8 @@ | |||
<div class="container"> | |||
<div class="outputIcon"> | |||
<mat-icon>volume_up</mat-icon> | |||
</div> | |||
<div class="outputText"> | |||
<div>{{outputText}}</div> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { OutputComponent } from './output.component'; | |||
// describe('OutputComponent', () => { | |||
// let component: OutputComponent; | |||
// let fixture: ComponentFixture<OutputComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ OutputComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(OutputComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,38 @@ | |||
import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
import { Subscription } from 'rxjs'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
declare var speechSynthesis: any; | |||
@Component({ | |||
selector: 'app-output', | |||
templateUrl: './output.component.html', | |||
styleUrls: ['./output.component.css'] | |||
}) | |||
export class OutputComponent implements OnInit, OnDestroy { | |||
subscription: Subscription; | |||
outputText: string = ''; | |||
constructor(private mqttRequestService: MqttRequestService) { } | |||
ngOnInit(): void { | |||
this.subscription = this.mqttRequestService.responseMessage.subscribe(msg => { | |||
setTimeout(() => { | |||
this.outputText = msg; | |||
this.speak(this.outputText); | |||
}, 3000); | |||
}); | |||
} | |||
speak(text: string) { | |||
const utterance = new SpeechSynthesisUtterance(text); | |||
utterance.lang = 'en-US'; | |||
speechSynthesis.speak(utterance); | |||
} | |||
ngOnDestroy() { | |||
this.subscription.unsubscribe(); | |||
this.mqttRequestService.onDestroy(); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
// import { TestBed } from '@angular/core/testing'; | |||
// import { SpeechService } from './speech.service'; | |||
// describe('SpeechService', () => { | |||
// let service: SpeechService; | |||
// beforeEach(() => { | |||
// TestBed.configureTestingModule({}); | |||
// service = TestBed.inject(SpeechService); | |||
// }); | |||
// it('should be created', () => { | |||
// expect(service).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,75 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { BehaviorSubject } from 'rxjs-compat'; | |||
declare var webkitSpeechRecognition: any; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class SpeechService { | |||
private approvalStageMessage = new BehaviorSubject(''); | |||
currentApprovalStageMessage = this.approvalStageMessage.asObservable(); | |||
initSpeechServiceStarted: boolean = false; | |||
recognition = new webkitSpeechRecognition(); | |||
isStoppedSpeechRecog = false; | |||
public text = ''; | |||
tempWords: string | undefined; | |||
constructor() { | |||
this.recognition.pitch = 10; | |||
this.recognition.rate = 3; | |||
} | |||
init() { | |||
this.initSpeechServiceStarted = true; | |||
this.recognition.interimResults = true; | |||
this.recognition.lang = 'en-US'; | |||
this.recognition.addEventListener('result', (e: { results: Iterable<unknown> | ArrayLike<unknown>; }) => { | |||
const transcript = Array.from(e.results) | |||
.map((result: any) => result[0]) | |||
.map((result) => result.transcript) | |||
.join(''); | |||
this.tempWords = transcript; | |||
console.log('init:' + transcript); | |||
}); | |||
} | |||
start() { | |||
this.text = ''; | |||
this.isStoppedSpeechRecog = false; | |||
this.recognition.start(); | |||
console.log("Speech recognition started") | |||
this.recognition.addEventListener('end', () => { | |||
this.wordConcat() | |||
this.recognition.stop(); | |||
if (!this.isStoppedSpeechRecog) { | |||
this.isStoppedSpeechRecog = true; | |||
console.log("End speech recognition"); | |||
this.approvalStageMessage.next(this.text); | |||
} | |||
}); | |||
} | |||
stop() { | |||
this.recognition.stop(); | |||
if (!this.isStoppedSpeechRecog) { | |||
this.isStoppedSpeechRecog = true; | |||
console.log("End speech recognition"); | |||
this.approvalStageMessage.next(this.text); | |||
} | |||
} | |||
wordConcat() { | |||
this.text = this.text + ' ' + this.tempWords; | |||
this.tempWords = ''; | |||
} | |||
onDestory() { | |||
this.text = ''; | |||
this.approvalStageMessage.next(this.text); | |||
this.isStoppedSpeechRecog = false; | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
div { | |||
height: 70%; | |||
width: 80%; | |||
margin: 2% 9%; | |||
} |
@@ -0,0 +1 @@ | |||
<div #mapContainer></div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { MapComponent } from './map.component'; | |||
// describe('MapComponent', () => { | |||
// let component: MapComponent; | |||
// let fixture: ComponentFixture<MapComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [MapComponent] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(MapComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,23 @@ | |||
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; | |||
import * as L from 'leaflet'; | |||
import 'leaflet/dist/leaflet.css'; | |||
@Component({ | |||
selector: 'app-map', | |||
templateUrl: './map.component.html', | |||
styleUrls: ['./map.component.css'] | |||
}) | |||
export class MapComponent implements AfterViewInit { | |||
@ViewChild('mapContainer', { static: false }) mapContainer: ElementRef; | |||
ngAfterViewInit(): void { | |||
const map = L.map(this.mapContainer.nativeElement).setView([51.505, -0.09], 13); | |||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |||
}).addTo(map); | |||
L.marker([49.452785, 11.096963]).addTo(map) //Koordinaten [X,Y] | |||
.bindPopup('Robot') | |||
.openPopup(); | |||
} | |||
} |
@@ -0,0 +1,85 @@ | |||
.IMG-Plants { | |||
border-radius: 0.03125rem; | |||
width: 9.25rem; | |||
height: 7.375rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
.icons-measurements { | |||
width: 1rem; | |||
height: 0.75rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
.container { | |||
min-width: 40rem; | |||
width: 100%; | |||
min-height: 9rem; | |||
padding: 0.3125rem; | |||
box-shadow: 0.3125rem 0.3125rem 0.3125rem silver; | |||
border-radius: 1.25rem; | |||
border-color: silver; | |||
border-width: 0.0625rem; | |||
border-style: solid; | |||
margin-bottom: 1rem; | |||
font-family: Cormorant; | |||
display: grid; | |||
grid-template-columns: 45% 50% 5%; | |||
justify-content: center; | |||
align-content: center; | |||
font-size: 1.125rem; | |||
} | |||
.item-header-content { | |||
grid-column: 1/2; | |||
justify-self: center; | |||
align-self: center; | |||
display: grid; | |||
width: 100%; | |||
grid-template-columns: 55% 45%; | |||
grid-template-rows: 25% 75%; | |||
justify-content: start; | |||
align-content: start; | |||
} | |||
.item-header { | |||
grid-column: 1/3; | |||
grid-row: 1/2; | |||
justify-self: center; | |||
align-self: start; | |||
font-size: 1.875rem; | |||
margin-bottom: 1rem; | |||
} | |||
.item-Measurements-IconsAndNames { | |||
grid-column: 1/2; | |||
grid-row: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-Measurements-Values { | |||
grid-column: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-IMG { | |||
justify-self: center; | |||
align-self: end; | |||
} | |||
.item-delete { | |||
justify-self: end; | |||
align-self: center; | |||
} | |||
.delete-icon { | |||
transform: scale(1.2); | |||
} | |||
p { | |||
margin: 0; | |||
} |
@@ -0,0 +1,38 @@ | |||
<div class="container"> | |||
<div class="item-header-content"> | |||
<div class="item-header"> | |||
<p class="card-title">{{plant.PlantName}}</p> | |||
</div> | |||
<div class="item-Measurements-IconsAndNames"> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_humidity_icon.png" alt="not available"> | |||
Air Humidity: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_temperature_icon.png" alt="not available"> | |||
Air Temperature: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/soil_moisture_icon.png" alt="not available"> | |||
Soil Moisture: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/brightness_icon.png" alt="not available"> | |||
Brightness:<br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/Timestamp_icon.png" alt="not available"> | |||
Timestamp: | |||
</div> | |||
<div class="item-Measurements-Values"> | |||
<p>{{plant.AirHumidity}} %</p> | |||
<p>{{plant.AirTemperature}} °C</p> | |||
<p>{{plant.SoilMoisture}} %</p> | |||
<p>{{plant.Brightness}} Lux</p> | |||
<p>{{getFormattedLastRequestedTimestamp() | async}}</p> | |||
</div> | |||
</div> | |||
<div class="item-IMG"> | |||
<img class="IMG-Plants" src="assets/images/gruppe1.jpg" alt="Plant is not available"> | |||
<img class="IMG-Plants" src="assets/images/gruppe2.jpg" alt="Plant is not available"> | |||
</div> | |||
<div class="item-delete"> | |||
<button class="item-Configure" mat-icon-button color="primary" (click)="onConfigurePlant()"> | |||
<mat-icon>create</mat-icon> | |||
</button> | |||
<button mat-icon-button color="warn" (click)="onDeletePlant()"> | |||
<mat-icon class="delete-icon">delete</mat-icon> | |||
</button> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { PlantCardComponent } from './plant-card.component'; | |||
// describe('PlantCardComponent', () => { | |||
// let component: PlantCardComponent; | |||
// let fixture: ComponentFixture<PlantCardComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [PlantCardComponent] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(PlantCardComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,62 @@ | |||
import { Component, Input } from '@angular/core'; | |||
import { MatDialog } from '@angular/material/dialog'; | |||
import { Observable, of } from 'rxjs'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { ConfigurePlantDialogComponent } from 'src/app/dialog/configure-plant-dialog/configure-plant-dialog.component'; | |||
import { DeleteConfirmDialogComponent } from 'src/app/dialog/delete-confirm-dialog/delete-confirm-dialog.component'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
@Component({ | |||
selector: 'app-plant-card', | |||
templateUrl: './plant-card.component.html', | |||
styleUrls: ['./plant-card.component.css'] | |||
}) | |||
export class PlantCardComponent { | |||
@Input() plant: Plant; | |||
currentDate: Date; | |||
constructor(public dialog: MatDialog, private mqttRequestService: MqttRequestService) { | |||
} | |||
getFormattedLastRequestedTimestamp(): Observable<string> { | |||
return of(this.calculateTimestamp(this.plant.Timestamp)) | |||
} | |||
calculateTimestamp(timestamp: string): string { | |||
const datePlant = new Date(timestamp); | |||
this.currentDate = new Date(); | |||
let seconds = ((this.currentDate.getTime() - datePlant.getTime()) / (1000)); | |||
const minutes = Math.floor(seconds / 60); | |||
const hours = Math.floor(minutes / 60); | |||
const days = Math.floor(hours / 24); | |||
if (days >= 1) { | |||
return `${days} day${days > 1 ? 's' : ''} ago`; | |||
} else if (hours >= 1) { | |||
return `${hours} hour${hours > 1 ? 's' : ''} ago`; | |||
} else if (minutes >= 1) { | |||
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; | |||
} else { | |||
seconds = Math.floor(seconds); | |||
return `${seconds} second${seconds !== 1 ? 's' : ''} ago`; | |||
} | |||
} | |||
onDeletePlant() { | |||
this.dialog.open(DeleteConfirmDialogComponent, { | |||
data: this.plant, | |||
width: '400px', | |||
}); | |||
} | |||
onConfigurePlant() { | |||
this.dialog.open(ConfigurePlantDialogComponent, { | |||
data: { | |||
plant: this.plant | |||
}, | |||
width: '400px', | |||
}); | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
.container { | |||
display: grid; | |||
width: 100%; | |||
grid-template-columns: 50% 50%; | |||
grid-template-rows: 20% 80%; | |||
margin-top: 3.125rem; | |||
} | |||
.buttonSearch { | |||
background-color: white; | |||
border: none; | |||
} | |||
.item-Search { | |||
grid-row: 1 / 2; | |||
grid-column: 1/2; | |||
justify-self: end; | |||
align-self: center; | |||
margin-right: 8rem; | |||
} | |||
.item-NewPlant { | |||
grid-row: 1 / 2; | |||
grid-column: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
margin-left: 18rem; | |||
} | |||
.item-Cards { | |||
grid-row: 2 / -1; | |||
grid-column: 1/3; | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
mat-spinner { | |||
margin: 7rem auto; | |||
} |
@@ -0,0 +1,32 @@ | |||
<div class="container" *ngIf="(plants$ |async) !== undefined; else errorLoadingData"> | |||
<div class="item-Search"> | |||
<form class="example-form"> | |||
<mat-form-field class=" example-full-width" appearance="fill"> | |||
<input type="text" placeholder="Plant Name" aria-label="Plant Name" matInput [formControl]="myControl" | |||
[matAutocomplete]="auto"> | |||
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" | |||
(optionSelected)='onOptionSelect($event.option.value)'> | |||
<mat-option *ngFor="let option of filteredOptions | async" [value]="option"> | |||
{{option}} | |||
</mat-option> | |||
</mat-autocomplete> | |||
</mat-form-field> | |||
<button class="buttonSearch"> <mat-icon matSuffix>search</mat-icon></button> | |||
</form> | |||
</div> | |||
<div class="example-button-row item-NewPlant"> | |||
<button class="buttonNew" mat-raised-button color="primary" (click)="onNewPlant()">New Plant</button> | |||
</div> | |||
<div class="item-Cards"> | |||
<div *ngIf="selectedPlant === 'undefined'; else elseBlock"> | |||
<app-plant-card *ngFor="let plant of (plants$ | async)" [plant]="plant"></app-plant-card> | |||
</div> | |||
<ng-template #elseBlock> | |||
<app-plant-card [plant]="onSearchPlant(selectedPlant) | async"></app-plant-card> | |||
<button mat-raised-button color="primary" (click)="onOptionDefault()">Show all | |||
Plants</button> | |||
</ng-template> | |||
</div> | |||
</div> | |||
<ng-template #errorLoadingData><mat-spinner></mat-spinner> | |||
</ng-template> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { PlantsComponent } from './plants.component'; | |||
// describe('PlantsComponent', () => { | |||
// let component: PlantsComponent; | |||
// let fixture: ComponentFixture<PlantsComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ PlantsComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(PlantsComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,56 @@ | |||
import { Component } from '@angular/core'; | |||
import { FormControl } from '@angular/forms'; | |||
import { MatDialog } from '@angular/material/dialog'; | |||
import { Observable, map, startWith } from 'rxjs'; | |||
import { MqttRequestService } from '../Service/Mqtt/mqtt-request.service'; | |||
import { StoreService } from '../Service/store.service'; | |||
import { AddPlantDialogComponent } from '../dialog/add-plant-dialog/add-plant-dialog.component'; | |||
import { Plant } from '../models/plant.model'; | |||
@Component({ | |||
selector: 'app-plants', | |||
templateUrl: './plants.component.html', | |||
styleUrls: ['./plants.component.css'] | |||
}) | |||
export class PlantsComponent { | |||
myControl = new FormControl(''); | |||
options$: Observable<string[]> = this.storeService.getAllPlantNames(); | |||
selectedPlant: string = 'undefined'; | |||
filteredOptions: Observable<string[]>; | |||
plants$: Observable<Plant[]> = this.storeService.currentPlants; | |||
constructor(public dialog: MatDialog, public storeService: StoreService, private mqttRequestService: MqttRequestService) { | |||
this.options$.subscribe(response => { | |||
this.filteredOptions = this.myControl.valueChanges.pipe( | |||
startWith(''), | |||
map(value => this._filter(value || '', response)), | |||
); | |||
}) | |||
} | |||
onSearchPlant(plantName: string): Observable<Plant> { | |||
return this.storeService.getPlant(plantName); | |||
} | |||
onNewPlant() { | |||
this.dialog.open(AddPlantDialogComponent, { | |||
width: '420px', | |||
}); | |||
} | |||
onOptionSelect(value: string) { | |||
this.selectedPlant = value; | |||
} | |||
onOptionDefault() { | |||
this.selectedPlant = 'undefined'; | |||
} | |||
private _filter(value: string, options: string[]): string[] { | |||
const filterValue = value.toLowerCase(); | |||
return options?.filter(option => option.toLowerCase().includes(filterValue)); | |||
} | |||
} |
@@ -0,0 +1,88 @@ | |||
img { | |||
border-radius: 0.5rem; | |||
width: 15.625rem; | |||
height: 12.5rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
.container { | |||
text-align: center; | |||
display: grid; | |||
grid-template-columns: 40.625rem 12.5rem; | |||
grid-template-rows: 12.5rem 6.25rem 6.25rem; | |||
width: 53.125rem; | |||
min-width: 46.875rem; | |||
font-family: Cormorant; | |||
box-shadow: 0.3125rem 0.3125rem 0.3125rem silver; | |||
border-radius: 1.25rem; | |||
border-color: silver; | |||
border-width: 0.0625rem; | |||
border-style: solid; | |||
padding: 0.625rem; | |||
margin: auto; | |||
margin-top: 3rem; | |||
} | |||
.robotImage { | |||
grid-row: 1 / -1; | |||
grid-column: 2 / 3; | |||
justify-self: end; | |||
align-self: start; | |||
} | |||
.robotAkku { | |||
grid-row: 1 / 2; | |||
grid-column: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
display: grid; | |||
grid-template-columns: 15.625rem 12.5rem 12.5rem; | |||
align-items: center; | |||
} | |||
.akkuMsg { | |||
grid-column: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
margin: 0; | |||
} | |||
.akkuProgressbar { | |||
grid-column: 2/3; | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
.akkuValue { | |||
grid-column: 3/4; | |||
justify-self: start; | |||
align-self: center; | |||
margin: 0; | |||
margin-left: 0.3125rem; | |||
} | |||
.robotCoordinates { | |||
grid-row: 2 / 3; | |||
grid-column: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
display: grid; | |||
grid-template-columns: 15.625rem 12.5rem; | |||
align-items: center; | |||
} | |||
.item-timestamp-name { | |||
grid-column: 1/2; | |||
justify-self: start; | |||
align-self: start; | |||
} | |||
.item-timestamp-value { | |||
grid-column: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
mat-spinner { | |||
margin: 7rem auto; | |||
} |
@@ -0,0 +1,18 @@ | |||
<div class="container" | |||
*ngIf="robotBatteryResponse$ !== undefined && (robotBatteryResponse$ |async) !== undefined; else errorLoadingData"> | |||
<div class="robotImage"> | |||
<img src="assets/images/gruppe1.jpg" alt="Plant is not available"> | |||
</div> | |||
<div class="robotAkku"> | |||
<h1 class="akkuMsg">Robot Akku:</h1> | |||
<mat-progress-bar class="akkuProgressbar" mode="determinate" [value]="batteryChargingStatus$ | async" | |||
[color]="(getLoadingBarColor() | async)"></mat-progress-bar> | |||
<h3 class="akkuValue">{{batteryChargingStatus$ | async}}%</h3> | |||
</div> | |||
<div class="robotCoordinates"> | |||
<h1 class="item-timestamp-name">Timestamp:</h1> | |||
<h1 class="item-timestamp-value">{{getFormattedLastRequestedBatteryState()| async}}</h1> | |||
</div> | |||
</div> | |||
<ng-template #errorLoadingData><mat-spinner></mat-spinner> | |||
</ng-template> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { RobotComponent } from './robot.component'; | |||
// describe('RobotComponent', () => { | |||
// let component: RobotComponent; | |||
// let fixture: ComponentFixture<RobotComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ RobotComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(RobotComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,53 @@ | |||
import { Component } from '@angular/core'; | |||
import { ThemePalette } from '@angular/material/core'; | |||
import { Observable, map } from 'rxjs'; | |||
import { StoreService } from '../Service/store.service'; | |||
import { RobotBattery } from '../models/robotBattery.model'; | |||
@Component({ | |||
selector: 'app-robot', | |||
templateUrl: './robot.component.html', | |||
styleUrls: ['./robot.component.css'] | |||
}) | |||
export class RobotComponent { | |||
robotBatteryResponse$: Observable<RobotBattery> = this.storeService.currentRobotBattery; | |||
batteryChargingStatus$: Observable<number> = this.robotBatteryResponse$.pipe(map(response => response.Battery)); | |||
requestTimestamp$: Observable<string> = this.robotBatteryResponse$.pipe(map(response => response.Timestamp)); | |||
constructor(private storeService: StoreService) { | |||
} | |||
getLoadingBarColor(): Observable<ThemePalette> { | |||
return this.batteryChargingStatus$.pipe( | |||
map(chargingStatus => chargingStatus < 20 ? "warn" : "primary") | |||
) | |||
} | |||
getFormattedLastRequestedBatteryState(): Observable<string> { | |||
return this.requestTimestamp$.pipe( | |||
map(timestamp => this.formatLastRequestedBatteryState(timestamp)) | |||
) | |||
} | |||
formatLastRequestedBatteryState(timestamp: string): string { | |||
const datePlant = new Date(timestamp); | |||
const currentDate = new Date(); | |||
let seconds = ((currentDate.getTime() - datePlant.getTime()) / (1000)); | |||
const minutes = Math.floor(seconds / 60); | |||
const hours = Math.floor(minutes / 60); | |||
const days = Math.floor(hours / 24); | |||
if (days >= 1) { | |||
return `${days} day${days > 1 ? 's' : ''} ago`; | |||
} else if (hours >= 1) { | |||
return `${hours} hour${hours > 1 ? 's' : ''} ago`; | |||
} else if (minutes >= 1) { | |||
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; | |||
} else { | |||
seconds = Math.floor(seconds); | |||
return `${seconds} second${seconds !== 1 ? 's' : ''} ago`; | |||
} | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
// import { TestBed } from '@angular/core/testing'; | |||
// import { MqttRequestService } from './mqtt-request.service'; | |||
// describe('MqttRequestService', () => { | |||
// let service: MqttRequestService; | |||
// beforeEach(() => { | |||
// TestBed.configureTestingModule({}); | |||
// service = TestBed.inject(MqttRequestService); | |||
// }); | |||
// it('should be created', () => { | |||
// expect(service).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,222 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { MatDialog } from '@angular/material/dialog'; | |||
import { BehaviorSubject, Subscription } from 'rxjs'; | |||
import { ErrorDialogComponent } from 'src/app/dialog/error-dialog/error-dialog.component'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
import { PlantCount } from 'src/app/models/plantCount.model'; | |||
import { Position } from 'src/app/models/position.model'; | |||
import { RobotBattery } from 'src/app/models/robotBattery.model'; | |||
import { StoreService } from '../store.service'; | |||
import { MqttSetDataService } from './mqtt-set-data.service'; | |||
import { MqttService } from './mqtt.service'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class MqttRequestService { | |||
private sourceMessage = new BehaviorSubject(''); | |||
responseMessage = this.sourceMessage.asObservable(); | |||
timerSubscription: Subscription | undefined; | |||
constructor(private mqttService: MqttService, | |||
private mqttSetDataService: MqttSetDataService, | |||
private storeService: StoreService, public dialog: MatDialog) { | |||
//Subscribe to the Topics | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/SENSORDATA_ALL").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as Plant[]; | |||
this.mqttSetDataService.setDataAllPlants(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/ERROR").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as string; | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: payload, | |||
width: '400px', | |||
}); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/SENSORDATA").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as Plant; | |||
this.mqttSetDataService.setDataPlant(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/POSITION").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as Position; | |||
this.mqttSetDataService.setRobotPosition(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/BATTERY").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as RobotBattery; | |||
this.mqttSetDataService.setRobotBattery(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/ROBOTREADY").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as boolean; | |||
this.mqttSetDataService.setRobotReady(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
this.mqttService.subscribeToTopic("BACKEND/DATA/PLANTCOUNT").subscribe(data => { | |||
if (typeof data !== "object") { | |||
const payload = JSON.parse(data) as PlantCount; | |||
this.mqttSetDataService.setPlantCount(payload); | |||
} | |||
}, err => { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: err, | |||
width: '400px', | |||
}); | |||
console.error('Error:', err); | |||
}); | |||
// this.mqttService.subscribeToTopic("BACKEND/DATA/POSITION").subscribe(data => { | |||
// if (typeof data !== "object") { | |||
// const payload = JSON.parse(data) as Plant[]; | |||
// this.mqttSetDataService.setDataAllPlants(payload); | |||
// } | |||
// }, err => { | |||
// console.error('Error:', err); | |||
// }); | |||
// this.mqttService.subscribeToTopic("BACKEND/DATA/PICTURE").subscribe(data => { | |||
// if (typeof data !== "object") { | |||
// const payload = JSON.parse(data) as Plant; | |||
// this.mqttSetDataService.setDataPlant(payload); | |||
// } | |||
// }, err => { | |||
// console.error('Error:', err); | |||
// }); | |||
//Request to get ALL DATA every 10sec!! | |||
// this.timerSubscription = timer(0, 10000).pipe( | |||
// map(() => { | |||
// this.mqttService.publishToTopic('BACKEND/ACTION/GETALLDATA'); | |||
// }) | |||
// ).subscribe(); | |||
//Publish to the Topic to recieve the first Data | |||
this.publishToPLANTCOUNT(); | |||
this.publishToGETBATTERY(); | |||
this.publishToROBOTREADY(); | |||
this.mqttService.publishToTopic('BACKEND/ACTION/GETALLDATA', JSON.stringify(this.storeService.getAllPlantNames())); | |||
} | |||
publishToGETALLDATA() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/GETALLDATA', JSON.stringify(this.storeService.getAllPlantNames())); | |||
} | |||
publishToDRIVE() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/DRIVE'); | |||
} | |||
publishToGETBATTERY() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/GETBATTERY'); | |||
} | |||
publishToROBOTREADY() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/ROBOTREADY'); | |||
} | |||
publishToGETPOSITION() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/GETPOSITION'); | |||
} | |||
publishToNEWPLANT(plant: Plant) { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/NEWPLANT', JSON.stringify(plant)); | |||
} | |||
publishToCONFIGUREPLANT(plant: Plant) { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/CONFIGUREPLANT', JSON.stringify(plant)); | |||
} | |||
publishToDELETEPLANT(plantID: number) { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/DELETEPLANT', JSON.stringify(plantID)); | |||
} | |||
publishToPLANTCOUNT() { | |||
this.mqttService.publishToTopic('BACKEND/ACTION/PLANTCOUNT'); | |||
} | |||
publishToINPUT(message: string) { | |||
message = message.replace(/\s/g, ""); | |||
console.log(":" + message + ":"); | |||
let msgFound: boolean = false; | |||
if (message === "whereismyrobot") { | |||
msgFound = true; | |||
this.mqttService.publishToTopic("BACKEND/ACTION/GETPOSITION") | |||
this.sourceMessage.next("The data is being collected and you can view it under the 'Robot' tab"); | |||
} else if (message === "howmuchbatterydoesmyrobothave") { | |||
msgFound = true; | |||
this.mqttService.publishToTopic('BACKEND/ACTION/GETBATTERY'); | |||
this.sourceMessage.next("The data is being collected and you can view it under the 'Robot' tab"); | |||
} else if (message === "howaremyplants") { | |||
msgFound = true; | |||
// this.mqttService.publishToTopic('BACKEND/ACTION/DRIVEALL'); | |||
this.sourceMessage.next("Robot is driving to all plants"); | |||
} else { | |||
this.storeService.getAllPlantNames().subscribe(result => | |||
result.forEach((element: string) => { | |||
let msgPlantName = "howismyplant".concat(element.toLowerCase()) | |||
if (message === msgPlantName) { | |||
msgFound = true; | |||
let PlantName = element; | |||
this.mqttService.publishToTopic('BACKEND/ACTION/DRIVE', JSON.stringify(PlantName)); | |||
this.sourceMessage.next("Robot is driving to plant " + element); | |||
} | |||
}) | |||
) | |||
} | |||
if (!msgFound) { | |||
this.sourceMessage.next("Please try again"); | |||
} | |||
} | |||
onDestroy() { | |||
this.sourceMessage.next(''); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
// import { TestBed } from '@angular/core/testing'; | |||
// import { MqttSetDataService } from './mqtt-set-data.service'; | |||
// describe('MqttSetDataService', () => { | |||
// let service: MqttSetDataService; | |||
// beforeEach(() => { | |||
// TestBed.configureTestingModule({}); | |||
// service = TestBed.inject(MqttSetDataService); | |||
// }); | |||
// it('should be created', () => { | |||
// expect(service).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,36 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
import { PlantCount } from 'src/app/models/plantCount.model'; | |||
import { Position } from 'src/app/models/position.model'; | |||
import { RobotBattery } from 'src/app/models/robotBattery.model'; | |||
import { StoreService } from '../store.service'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class MqttSetDataService { | |||
constructor(private storeService: StoreService) { } | |||
setDataAllPlants(data: Plant[]): void { | |||
this.storeService.setDataAllPlants(data); | |||
} | |||
setDataPlant(data: Plant): void { | |||
this.storeService.setDataPlant(data); | |||
} | |||
setRobotBattery(data: RobotBattery): void { | |||
this.storeService.setRobotState(data); | |||
} | |||
setRobotReady(data: boolean): void { | |||
this.storeService.setRobotReady(data); | |||
} | |||
setRobotPosition(data: Position): void { | |||
this.storeService.setRobotPosition(data); | |||
} | |||
setPlantCount(data: PlantCount): void { | |||
this.storeService.setPlantCount(data); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
// import { TestBed } from '@angular/core/testing'; | |||
// import { MqttService } from './mqtt.service'; | |||
// describe('MqttService', () => { | |||
// let service: MqttService; | |||
// beforeEach(() => { | |||
// TestBed.configureTestingModule({}); | |||
// service = TestBed.inject(MqttService); | |||
// }); | |||
// it('should be created', () => { | |||
// expect(service).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,37 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { connect, MqttClient } from 'mqtt/dist/mqtt'; | |||
import { Observable } from 'rxjs'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class MqttService { | |||
private client: MqttClient; | |||
constructor() { | |||
this.client = connect('wss://mqtt.eclipseprojects.io:443/mqtt'); //Je nachdem welchen Link der Broker hat | |||
// this.client = connect('mqtt://192.168.137.197:1883', { clientId: 'kemal' }); | |||
} | |||
public subscribeToTopic(topic: string): Observable<any> { | |||
return new Observable(observer => { | |||
this.client.subscribe(topic, (err, granted) => { | |||
if (err) { | |||
observer.error(err); | |||
} else { | |||
observer.next(granted); | |||
} | |||
}); | |||
this.client.on('message', (t, m) => { | |||
if (t === topic) { | |||
observer.next(m.toString()); | |||
} | |||
}); | |||
}); | |||
} | |||
public publishToTopic(topic: string, message?: any): void { | |||
this.client.publish(topic, message); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
import { TestBed } from '@angular/core/testing'; | |||
import { StoreService } from './store.service'; | |||
describe('StoreService', () => { | |||
let service: StoreService; | |||
beforeEach(() => { | |||
TestBed.configureTestingModule({}); | |||
service = TestBed.inject(StoreService); | |||
}); | |||
it('should be created', () => { | |||
expect(service).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,96 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { BehaviorSubject, Observable, map, of } from 'rxjs'; | |||
import { Plant } from '../models/plant.model'; | |||
import { PlantCount } from '../models/plantCount.model'; | |||
import { Position } from '../models/position.model'; | |||
import { RobotBattery } from '../models/robotBattery.model'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class StoreService { | |||
positionRobot: Position; | |||
plantCount: PlantCount; | |||
private plants: BehaviorSubject<Plant[]> = new BehaviorSubject<Plant[]>(undefined); | |||
currentPlants: Observable<Plant[]> = this.plants.asObservable(); | |||
private robotReady = new BehaviorSubject<boolean>(undefined); | |||
currentRobotReady = this.robotReady.asObservable(); | |||
private robotBattery = new BehaviorSubject<RobotBattery>(undefined); | |||
currentRobotBattery = this.robotBattery.asObservable(); | |||
constructor() { | |||
} | |||
getPlant(name: string): Observable<Plant> { | |||
return this.currentPlants.pipe(map(response => { | |||
for (let plant of response) { | |||
if (plant.PlantName.toLocaleLowerCase() === name.toLocaleLowerCase()) { | |||
return plant; | |||
} | |||
} | |||
return null; | |||
})) | |||
} | |||
getAllPlantNames(): Observable<string[]> { | |||
return this.currentPlants.pipe(map(response => response?.map(response => response.PlantName))); | |||
} | |||
getIDsWithoutPlant(plants: Plant[]): Observable<number[]> { | |||
let listOfID: number[] = []; | |||
let listOfIDWithoutPlant: number[] = []; | |||
if (plants.length > 0) { | |||
//Write ID's of the Plants in a List | |||
plants.forEach(plant => { | |||
listOfID.push(plant.PlantID); | |||
}); | |||
//Sort the List of ID's | |||
listOfID.sort(); | |||
//Write the ID's without Plants in a List | |||
for (let index = 0; index < this.plantCount.MaxCount; index++) { //Noch unsicher.... evtl. die länge von wo anders holen.. | |||
if (!listOfID.includes(index + 1)) { | |||
listOfIDWithoutPlant.push(index + 1); | |||
} | |||
} | |||
return of(listOfIDWithoutPlant); | |||
} else { | |||
for (let index = 0; index < this.plantCount.MaxCount; index++) { | |||
listOfIDWithoutPlant.push(index + 1); | |||
} | |||
return of(listOfIDWithoutPlant); | |||
} | |||
} | |||
//Set data | |||
setDataAllPlants(data: Plant[]): void { | |||
this.plants.next(data); | |||
} | |||
setDataPlant(data: Plant): void { | |||
this.plants.subscribe(response => { | |||
response.forEach(plant => { | |||
if (plant.PlantID === data.PlantID) { | |||
plant = data; | |||
} | |||
}) | |||
}) | |||
} | |||
setRobotState(data: RobotBattery): void { | |||
this.robotBattery.next(data); | |||
} | |||
setRobotReady(data: boolean): void { | |||
this.robotReady.next(data); | |||
} | |||
setRobotPosition(data: Position): void { | |||
this.positionRobot = data; | |||
} | |||
setPlantCount(data: PlantCount): void { | |||
this.plantCount = data; | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
import { NgModule } from '@angular/core'; | |||
import { RouterModule, Routes } from '@angular/router'; | |||
const routes: Routes = []; | |||
@NgModule({ | |||
imports: [RouterModule.forRoot(routes)], | |||
exports: [RouterModule] | |||
}) | |||
export class AppRoutingModule { } |
@@ -0,0 +1,44 @@ | |||
.container { | |||
margin: 50px; | |||
} | |||
.icon-menu { | |||
transform: scale(2); | |||
} | |||
span { | |||
font-size: 1.5rem; | |||
font-family: Cormorant; | |||
} | |||
header { | |||
border: 0.125rem; | |||
border-style: solid; | |||
border-radius: 1rem; | |||
border-color: #89bf54; | |||
box-shadow: 0.2rem 0.2rem 0.2rem silver; | |||
padding: 4px; | |||
display: grid; | |||
grid-template-columns: 20% 60% 20%; | |||
font-family: Cormorant; | |||
.menu-item { | |||
grid-column: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.header-item { | |||
grid-column: 2/3; | |||
justify-self: center; | |||
align-self: end; | |||
font-size: 3rem; | |||
} | |||
.img-item { | |||
grid-column: 3/4; | |||
justify-self: end; | |||
width: 15rem; | |||
height: 3rem; | |||
} | |||
} |
@@ -0,0 +1,36 @@ | |||
<html class="container"> | |||
<header> | |||
<div class="menu-item"> | |||
<button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Example icon-button with a menu"> | |||
<mat-icon class="icon-menu">menu</mat-icon> | |||
</button> | |||
<mat-menu #menu="matMenu"> | |||
<button mat-menu-item routerLink="/home" (click)="onMenuSelect('Home')"> | |||
<mat-icon>micro</mat-icon> | |||
<span>Home</span> | |||
</button> | |||
<button mat-menu-item routerLink="/map" (click)="onMenuSelect('Map')"> | |||
<mat-icon>public</mat-icon> | |||
<span>Map</span> | |||
</button> | |||
<button mat-menu-item routerLink="plants" (click)="onMenuSelect('Plants')"> | |||
<mat-icon>grass</mat-icon> | |||
<span>Plants</span> | |||
</button> | |||
<button mat-menu-item routerLink="robot" (click)="onMenuSelect('Robot')"> | |||
<mat-icon>android</mat-icon> | |||
<span>Robot</span> | |||
</button> | |||
</mat-menu> | |||
</div> | |||
<p class="header-item">{{header}}</p> | |||
<img class="img-item" src="assets/images/logo.png" alt="not available"> | |||
</header> | |||
<body> | |||
<router-outlet></router-outlet> | |||
</body> | |||
</html> |
@@ -0,0 +1,59 @@ | |||
import { Location } from '@angular/common'; | |||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
import { MatMenuModule } from '@angular/material/menu'; | |||
import { MqttRequestService } from './Service/Mqtt/mqtt-request.service'; | |||
import { AppComponent } from './app.component'; | |||
describe('AppComponent', () => { | |||
let component: AppComponent; | |||
let fixture: ComponentFixture<AppComponent>; | |||
let mqttRequestServiceSpy: jasmine.SpyObj<MqttRequestService>; | |||
let locationStub: Partial<Location>; | |||
beforeEach(async () => { | |||
const mqttRequestServiceMock = jasmine.createSpyObj('MqttRequestService', ['someMethod']); | |||
locationStub = { | |||
path: () => '/home' | |||
}; | |||
await TestBed.configureTestingModule({ | |||
imports: [MatMenuModule], | |||
declarations: [AppComponent], | |||
providers: [ | |||
{ provide: MqttRequestService, useValue: mqttRequestServiceMock }, | |||
{ provide: Location, useValue: locationStub } | |||
] | |||
}).compileComponents(); | |||
}); | |||
beforeEach(() => { | |||
fixture = TestBed.createComponent(AppComponent); | |||
component = fixture.componentInstance; | |||
mqttRequestServiceSpy = TestBed.inject(MqttRequestService) as jasmine.SpyObj<MqttRequestService>; | |||
fixture.detectChanges(); | |||
}); | |||
it('should create', () => { | |||
expect(component).toBeTruthy(); | |||
}); | |||
it('should set header to "Home" if the route is empty', () => { | |||
expect(component.header).toBe('Home'); | |||
}); | |||
it('should set header to the capitalized route value', () => { | |||
locationStub.path = () => '/about'; | |||
const componentNew = new AppComponent(mqttRequestServiceSpy, locationStub as Location); | |||
expect(componentNew.header).toBe('About'); | |||
}); | |||
it('should update header when onMenuSelect is called', () => { | |||
component.onMenuSelect('Map'); | |||
expect(component.header).toBe('Map'); | |||
}); | |||
}); |
@@ -0,0 +1,27 @@ | |||
import { Location } from '@angular/common'; | |||
import { Component } from '@angular/core'; | |||
import { MqttRequestService } from './Service/Mqtt/mqtt-request.service'; | |||
@Component({ | |||
selector: 'app-root', | |||
templateUrl: './app.component.html', | |||
styleUrls: ['./app.component.css'], | |||
}) | |||
export class AppComponent { | |||
header: string; | |||
constructor(private mqttRequestService: MqttRequestService, private location: Location) { | |||
const actualRoute = this.location.path().slice(1); | |||
const lastParam = actualRoute.charAt(0).toUpperCase() + actualRoute.slice(1); | |||
if (lastParam === "") { | |||
this.onMenuSelect('Home'); | |||
} else { | |||
this.onMenuSelect(lastParam); | |||
} | |||
} | |||
onMenuSelect(header: string) { | |||
this.header = header; | |||
} | |||
} | |||
@@ -0,0 +1,87 @@ | |||
import { NgModule } from '@angular/core'; | |||
import { BrowserModule } from '@angular/platform-browser'; | |||
import { ScrollingModule } from '@angular/cdk/scrolling'; | |||
import { HttpClientModule } from '@angular/common/http'; | |||
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; | |||
import { MatAutocompleteModule } from '@angular/material/autocomplete'; | |||
import { MatBadgeModule } from '@angular/material/badge'; | |||
import { MatButtonModule } from '@angular/material/button'; | |||
import { MatCardModule } from '@angular/material/card'; | |||
import { MatDialogModule } from '@angular/material/dialog'; | |||
import { MatFormFieldModule } from '@angular/material/form-field'; | |||
import { MatIconModule } from '@angular/material/icon'; | |||
import { MatInputModule } from '@angular/material/input'; | |||
import { MatMenuModule } from '@angular/material/menu'; | |||
import { MatProgressBarModule } from '@angular/material/progress-bar'; | |||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | |||
import { MatSelectModule } from '@angular/material/select'; | |||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | |||
import { RouterModule, Routes } from '@angular/router'; | |||
import { HomeComponent } from './Home/home.component'; | |||
import { InputComponent } from './Home/input/input.component'; | |||
import { OutputComponent } from './Home/output/output.component'; | |||
import { MapComponent } from './Map/map.component'; | |||
import { PlantCardComponent } from './Plants/plant-card/plant-card.component'; | |||
import { PlantsComponent } from './Plants/plants.component'; | |||
import { RobotComponent } from './Robot/robot.component'; | |||
import { AppRoutingModule } from './app-routing.module'; | |||
import { AppComponent } from './app.component'; | |||
import { AddPlantDialogComponent } from './dialog/add-plant-dialog/add-plant-dialog.component'; | |||
import { ConfigurePlantDialogComponent } from './dialog/configure-plant-dialog/configure-plant-dialog.component'; | |||
import { ErrorDialogComponent } from './dialog/error-dialog/error-dialog.component'; | |||
import { MapPlantDialogComponent } from './dialog/map-plant-dialog/map-plant-dialog.component'; | |||
import { DeleteConfirmDialogComponent } from './dialog/delete-confirm-dialog/delete-confirm-dialog.component'; | |||
const appRoutes: Routes = [ | |||
{ path: '', component: HomeComponent }, | |||
{ path: 'home', component: HomeComponent }, | |||
{ path: 'map', component: MapComponent }, | |||
{ path: 'plants', component: PlantsComponent }, | |||
{ path: 'robot', component: RobotComponent } | |||
]; | |||
@NgModule({ | |||
declarations: [ | |||
AppComponent, | |||
HomeComponent, | |||
MapComponent, | |||
InputComponent, | |||
OutputComponent, | |||
PlantsComponent, | |||
PlantCardComponent, | |||
RobotComponent, | |||
ConfigurePlantDialogComponent, | |||
AddPlantDialogComponent, | |||
MapPlantDialogComponent, | |||
ErrorDialogComponent, | |||
DeleteConfirmDialogComponent | |||
], | |||
imports: [ | |||
BrowserModule, | |||
AppRoutingModule, | |||
MatMenuModule, | |||
MatIconModule, | |||
MatButtonModule, | |||
HttpClientModule, | |||
BrowserAnimationsModule, | |||
MatBadgeModule, | |||
RouterModule.forRoot(appRoutes), | |||
MatFormFieldModule, | |||
FormsModule, | |||
MatInputModule, | |||
MatCardModule, | |||
ScrollingModule, | |||
MatProgressBarModule, | |||
MatAutocompleteModule, | |||
ReactiveFormsModule, | |||
MatDialogModule, | |||
MatSelectModule, | |||
MatProgressSpinnerModule | |||
], | |||
providers: [], | |||
bootstrap: [AppComponent], | |||
exports: [ConfigurePlantDialogComponent, AddPlantDialogComponent, MapPlantDialogComponent, ErrorDialogComponent] | |||
}) | |||
export class AppModule { } |
@@ -0,0 +1,78 @@ | |||
.item-header-content { | |||
display: grid; | |||
grid-template-columns: 11.25rem 6.25rem; | |||
grid-template-rows: 20% 80%; | |||
justify-content: start; | |||
align-content: start; | |||
} | |||
.item-header { | |||
grid-column: 1/-1; | |||
grid-row: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
font-size: 1.5625rem; | |||
margin-bottom: 1rem; | |||
color: blue; | |||
display: grid; | |||
grid-template-columns: 11.25rem 6.25rem; | |||
} | |||
.item-header-text { | |||
grid-column: 1/2; | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
.item-header-button { | |||
grid-column: 2/3; | |||
justify-self: end; | |||
background-color: white; | |||
transform: scale(0.8); | |||
} | |||
.icon-language { | |||
color: black; | |||
transform: scale(1.6); | |||
} | |||
.item-header-button:hover { | |||
background-color: silver; | |||
} | |||
.item-Measurements-IconsAndNames { | |||
grid-column: 1/2; | |||
grid-row: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
font-size: 1.125rem; | |||
} | |||
.item-Measurements-Values { | |||
grid-column: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-IMG { | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
p { | |||
margin: 0; | |||
} | |||
.IMG-Plants { | |||
border-radius: 0.5rem; | |||
width: 11.25rem; | |||
height: 9.375rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
.icons-measurements { | |||
width: 1rem; | |||
height: 0.75rem; | |||
margin: 0rem 0.3125rem; | |||
margin-top: 0.4rem; | |||
} |
@@ -0,0 +1,43 @@ | |||
<div class="item-header-content"> | |||
<div class="item-header"> | |||
<p class="item-header-text">Add Plant:</p> | |||
<button mat-fab class="item-header-button" (click)="onOpenMap()"><mat-icon | |||
class="icon-language">language</mat-icon></button> | |||
</div> | |||
<div class="item-Measurements-IconsAndNames"> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/id_icon.png" alt="not available"> | |||
ID: <br><br><br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/name_icon.png" alt="not available"> | |||
Name: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_humidity_icon.png" alt="not available"> | |||
Air Humidity: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_temperature_icon.png" alt="not available"> | |||
Air Temperature: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/soil_moisture_icon.png" alt="not available"> | |||
Soil Moisture: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/brightness_icon.png" alt="not available"> | |||
Brightness:<br> | |||
<!-- <img class="icons-measurements" src="assets/images/Icons-Card/Timestamp_icon.png" alt="not available"> | |||
Timestamp: --> | |||
</div> | |||
<div class="item-Measurements-Values"> | |||
<mat-form-field class="item-option" appearance="fill"> | |||
<mat-label>Choose an ID</mat-label> | |||
<mat-select [(value)]="selected" (closed)="onOptionSelected(selected)"> | |||
<mat-option *ngFor="let option of options$ | async" [value]="option"> | |||
{{option}} | |||
</mat-option> | |||
</mat-select> | |||
</mat-form-field> | |||
<input type="text" id="inputName" maxlength="15"> | |||
<input type="number" id="inputAirTemperature" max="100"> | |||
<input type="number" id="inputAirHumidity" max="100"> | |||
<input type="number" id="inputSoilMoisture" max="100"> | |||
<input type="number" id="inputBrightness" max="100"> | |||
</div> | |||
</div> | |||
<div mat-dialog-actions> | |||
<button mat-button mat-dialog-close>No</button> | |||
<button mat-button mat-dialog-close cdkFocusInitial (click)="onSaveDialog()" | |||
[disabled]="buttonSavedisabled">Save</button> | |||
</div> |
@@ -0,0 +1,172 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; | |||
// import { of } from 'rxjs'; | |||
// import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
// import { StoreService } from 'src/app/Service/store.service'; | |||
// import { Plant } from 'src/app/models/plant.model'; | |||
// import { AddPlantDialogComponent } from './add-plant-dialog.component'; | |||
// const plantMock1: Plant = { | |||
// AirTemperature: 25, | |||
// AirHumidity: 60, | |||
// SoilMoisture: 50, | |||
// Brightness: 80, | |||
// PlantID: 1, | |||
// Timestamp: "2023-06-05T12:00:00", | |||
// MeasurementID: "abcd1234", | |||
// PlantName: "Plant1", | |||
// }; | |||
// const plantMock2: Plant = { | |||
// AirTemperature: 22, | |||
// AirHumidity: 60, | |||
// SoilMoisture: 50, | |||
// Brightness: 80, | |||
// PlantID: 1, | |||
// Timestamp: "2023-06-05T12:00:00", | |||
// MeasurementID: "abcd1234", | |||
// PlantName: "Plant2", | |||
// }; | |||
// const plantMock3: Plant = { | |||
// AirTemperature: 21, | |||
// AirHumidity: 60, | |||
// SoilMoisture: 50, | |||
// Brightness: 80, | |||
// PlantID: 1, | |||
// Timestamp: "2023-06-05T12:00:00", | |||
// MeasurementID: "abcd1234", | |||
// PlantName: "Plant3", | |||
// }; | |||
// describe('AddPlantDialogComponent', () => { | |||
// let component: AddPlantDialogComponent; | |||
// let fixture: ComponentFixture<AddPlantDialogComponent>; | |||
// let dialogRefSpyObj: jasmine.SpyObj<MatDialogRef<AddPlantDialogComponent>>; | |||
// let dialogSpy: jasmine.SpyObj<MatDialog>; | |||
// let mqttRequestServiceSpy: jasmine.SpyObj<MqttRequestService>; | |||
// let storeServiceSpy: jasmine.SpyObj<StoreService>; | |||
// beforeEach(async () => { | |||
// const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['close']); | |||
// const dialogSpyObj = jasmine.createSpyObj('MatDialog', ['open']); | |||
// const mqttRequestServiceSpyObj = jasmine.createSpyObj('MqttRequestService', [ | |||
// 'publishToPLANTCOUNT', | |||
// 'publishToNEWPLANT', | |||
// 'publishToGETALLDATA', | |||
// ]); | |||
// const storeServiceSpyObj = jasmine.createSpyObj('StoreService', [ | |||
// 'currentPlants', | |||
// 'getIDsWithoutPlant', | |||
// 'getAllPlantNames', | |||
// ]); | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [AddPlantDialogComponent], | |||
// providers: [ | |||
// { provide: MatDialogRef, useValue: dialogRefSpy }, | |||
// { provide: MAT_DIALOG_DATA, useValue: {} }, | |||
// { provide: MatDialog, useValue: dialogSpyObj }, | |||
// { provide: MqttRequestService, useValue: mqttRequestServiceSpyObj }, | |||
// { provide: StoreService, useValue: storeServiceSpyObj }, | |||
// ], | |||
// }).compileComponents(); | |||
// }); | |||
// beforeEach(() => { | |||
// fixture = TestBed.createComponent(AddPlantDialogComponent); | |||
// component = fixture.componentInstance; | |||
// dialogRefSpyObj = TestBed.inject(MatDialogRef) as jasmine.SpyObj<MatDialogRef<AddPlantDialogComponent>>; | |||
// dialogSpy = TestBed.inject(MatDialog) as jasmine.SpyObj<MatDialog>; | |||
// mqttRequestServiceSpy = TestBed.inject(MqttRequestService) as jasmine.SpyObj<MqttRequestService>; | |||
// storeServiceSpy = TestBed.inject(StoreService) as jasmine.SpyObj<StoreService>; | |||
// }); | |||
// it('should initialize with buttonSavedisabled set to true', () => { | |||
// // Assert | |||
// expect(component.buttonSavedisabled).toBe(true); | |||
// }); | |||
// it('should call publishToPLANTCOUNT method and retrieve options', () => { | |||
// // Arrange | |||
// const currentPlantsMock = of([plantMock1, plantMock2, plantMock3]); | |||
// const getIDsWithoutPlantMock = jasmine.createSpy().and.returnValue(of([4, 5, 6])); | |||
// const getAllPlantNamesMock = jasmine.createSpy().and.returnValue(of(['Plant1', 'Plant2', 'Plant3'])); | |||
// // Act | |||
// storeServiceSpy.currentPlants = currentPlantsMock; | |||
// storeServiceSpy.getIDsWithoutPlant = getIDsWithoutPlantMock; | |||
// storeServiceSpy.getAllPlantNames = getAllPlantNamesMock; | |||
// fixture.detectChanges(); | |||
// // Assert | |||
// expect(mqttRequestServiceSpy.publishToPLANTCOUNT).toHaveBeenCalled(); | |||
// expect(storeServiceSpy.getIDsWithoutPlant).toHaveBeenCalledWith([plantMock1, plantMock2, plantMock3]); | |||
// expect(storeServiceSpy.getAllPlantNames).toHaveBeenCalled(); | |||
// expect(component.options$).toEqual(of([4, 5, 6])); | |||
// expect(component.plantNames).toEqual(['Plant1', 'Plant2', 'Plant3']); | |||
// }); | |||
// // it('should open the error dialog if plant input fails', () => { | |||
// // // Arrange | |||
// // const plantInputString = 'Plant Name is empty'; | |||
// // // Act | |||
// // component.plantInputTestFails = jasmine.createSpy().and.returnValue(plantInputString); | |||
// // component.onSaveDialog(); | |||
// // // Assert | |||
// // expect(dialogSpy.open).toHaveBeenCalledWith(ErrorDialogComponent, { | |||
// // data: 'Error: ' + plantInputString, | |||
// // width: '400px', | |||
// // }); | |||
// // expect(mqttRequestServiceSpy.publishToNEWPLANT).not.toHaveBeenCalled(); | |||
// // expect(mqttRequestServiceSpy.publishToGETALLDATA).not.toHaveBeenCalled(); | |||
// // expect(window.location.reload).not.toHaveBeenCalled(); | |||
// // }); | |||
// // it('should call the MQTT request service and reload the page if plant input is valid', () => { | |||
// // // Arrange | |||
// // const plantInputString = ''; | |||
// // // Act | |||
// // component.plantInputTestFails = jasmine.createSpy().and.returnValue(plantInputString); | |||
// // component.onSaveDialog(); | |||
// // // Assert | |||
// // expect(dialogSpy.open).not.toHaveBeenCalled(); | |||
// // expect(mqttRequestServiceSpy.publishToNEWPLANT).toHaveBeenCalledWith(component.plant); | |||
// // expect(mqttRequestServiceSpy.publishToGETALLDATA).toHaveBeenCalled(); | |||
// // expect(window.location.reload).toHaveBeenCalled(); | |||
// // }); | |||
// // it('should open the map plant dialog', () => { | |||
// // // Act | |||
// // component.onOpenMap(); | |||
// // // Assert | |||
// // expect(dialogSpy.open).toHaveBeenCalledWith(MapPlantDialogComponent, { | |||
// // width: '400px', | |||
// // height: '400px', | |||
// // }); | |||
// // }); | |||
// // it('should set buttonSavedisabled to false when option is selected within the range', () => { | |||
// // // Arrange | |||
// // component.buttonSavedisabled = true; | |||
// // // Act | |||
// // component.onOptionSelected(5); | |||
// // // Assert | |||
// // expect(component.buttonSavedisabled).toBeFalse(); | |||
// // }); | |||
// // it('should keep buttonSavedisabled as true when option is selected outside the range', () => { | |||
// // // Arrange | |||
// // component.buttonSavedisabled = true; | |||
// // // Act | |||
// // component.onOptionSelected(10); | |||
// // // Assert | |||
// // expect(component.buttonSavedisabled).toBeTrue(); | |||
// // }); | |||
// }); |
@@ -0,0 +1,100 @@ | |||
import { Component, Inject, OnInit } from '@angular/core'; | |||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; | |||
import { Observable } from 'rxjs'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { StoreService } from 'src/app/Service/store.service'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
import { ErrorDialogComponent } from '../error-dialog/error-dialog.component'; | |||
import { MapPlantDialogComponent } from '../map-plant-dialog/map-plant-dialog.component'; | |||
@Component({ | |||
selector: 'app-add-plant-dialog', | |||
templateUrl: './add-plant-dialog.component.html', | |||
styleUrls: ['./add-plant-dialog.component.css'] | |||
}) | |||
export class AddPlantDialogComponent implements OnInit { | |||
selected: number; | |||
options$: Observable<number[]>; | |||
plantNames: string[]; | |||
plant = {} as Plant; | |||
buttonSavedisabled: boolean; | |||
number: number[]; | |||
constructor(public dialogRef: MatDialogRef<AddPlantDialogComponent>, | |||
@Inject(MAT_DIALOG_DATA) public data: any, | |||
public dialog: MatDialog, | |||
private storeService: StoreService, | |||
private mqttRequestService: MqttRequestService) { | |||
} | |||
ngOnInit(): void { | |||
this.buttonSavedisabled = true; | |||
this.mqttRequestService.publishToPLANTCOUNT(); | |||
this.storeService.currentPlants.subscribe(response => { | |||
this.options$ = this.storeService.getIDsWithoutPlant(response); | |||
}) | |||
this.storeService.getAllPlantNames().subscribe(result => { | |||
this.plantNames = result | |||
}); | |||
} | |||
onSaveDialog() { | |||
this.plant.PlantName = (<HTMLInputElement>document.getElementById("inputName")).value; | |||
this.plant.AirTemperature = +(<HTMLInputElement>document.getElementById("inputAirTemperature")).value; | |||
this.plant.AirHumidity = +(<HTMLInputElement>document.getElementById("inputAirHumidity")).value; | |||
this.plant.SoilMoisture = +(<HTMLInputElement>document.getElementById("inputSoilMoisture")).value; | |||
this.plant.Brightness = +(<HTMLInputElement>document.getElementById("inputBrightness")).value; | |||
this.plant.PlantID = this.selected; | |||
const plantInputString: string = this.plantInputTestFails(); | |||
if (plantInputString) { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: "Error: " + plantInputString, | |||
width: '400px', | |||
}); | |||
} else { | |||
this.mqttRequestService.publishToNEWPLANT(this.plant); | |||
this.mqttRequestService.publishToGETALLDATA(); | |||
window.location.reload(); | |||
} | |||
} | |||
plantInputTestFails(): string { | |||
if (this.plantNames.indexOf(this.plant.PlantName) > -1) { | |||
return "Plant Name already exists"; | |||
} | |||
if (this.plant.PlantName === "") { | |||
return "Plant Name is empty"; | |||
} | |||
if (this.plant.AirTemperature > 100) { | |||
return "Air Temperature is to high"; | |||
} | |||
if (this.plant.AirHumidity > 100) { | |||
return "Air Humidity is to high"; | |||
} | |||
if (this.plant.SoilMoisture > 100) { | |||
return "Soil Moisture is to high"; | |||
} | |||
if (this.plant.Brightness > 100) { | |||
return "Brightness is to high"; | |||
} | |||
return ""; //Empty string is a Boolean false! | |||
} | |||
onOpenMap() { | |||
this.dialog.open(MapPlantDialogComponent, { | |||
width: '400px', | |||
height: '400px' | |||
}); | |||
} | |||
onOptionSelected(numb: number) { | |||
let num: number = numb; | |||
if (num < 7 && num > 0) { //TODOO anzahl der Pflanzen IDS variable machen statt 7 evtl. eine variable | |||
this.buttonSavedisabled = false; | |||
} | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
.item-header-content { | |||
display: grid; | |||
grid-template-columns: 11.25rem 6rem 3rem; | |||
grid-template-rows: 20% 80%; | |||
justify-content: start; | |||
align-content: start; | |||
} | |||
.item-header { | |||
grid-column: 1/-1; | |||
grid-row: 1/2; | |||
justify-self: start; | |||
align-self: center; | |||
font-size: 1.6rem; | |||
margin-bottom: 1rem; | |||
color: blue; | |||
} | |||
.item-Measurements-IconsAndNames { | |||
grid-column: 1/2; | |||
grid-row: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-Measurements-Values { | |||
grid-column: 2/3; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-Measurements-Units { | |||
grid-column: 3/4; | |||
justify-self: start; | |||
align-self: center; | |||
} | |||
.item-IMG { | |||
justify-self: center; | |||
align-self: center; | |||
} | |||
p { | |||
margin: 0; | |||
} | |||
.IMG-Plants { | |||
border-radius: 0.5rem; | |||
width: 11.25rem; | |||
height: 9.375rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
.icons-measurements { | |||
width: 1rem; | |||
height: 0.75rem; | |||
margin: 0rem 0.3125rem; | |||
} | |||
input { | |||
width: 5rem; | |||
} |
@@ -0,0 +1,34 @@ | |||
<div class="item-header-content"> | |||
<div class="item-header"> | |||
<p>Configure Plant: {{plant.PlantName}}</p> | |||
</div> | |||
<div class="item-Measurements-IconsAndNames"> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_humidity_icon.png" alt="not available"> | |||
Air Humidity: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/air_temperature_icon.png" alt="not available"> | |||
Air Temperature: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/soil_moisture_icon.png" alt="not available"> | |||
Soil Moisture: <br> | |||
<img class="icons-measurements" src="assets/images/Icons-Card/brightness_icon.png" alt="not available"> | |||
Brightness:<br> | |||
<!-- <img class="icons-measurements" src="assets/images/Icons-Card/Timestamp_icon.png" alt="not available"> | |||
Timestamp: --> | |||
</div> | |||
<div class="item-Measurements-Values"> | |||
<input type="number" [value]=plant.AirTemperature id="inputAirTemperature"> | |||
<input type="number" [value]=plant.AirHumidity id="inputAirHumidity"> | |||
<input type="number" [value]=plant.SoilMoisture id="inputSoilMoisture"> | |||
<input type="number" [value]=plant.Brightness id="inputBrightness"> | |||
<!-- <input type="datetime" [value]=plant.Timestamp id="inputTimestamp"> --> | |||
</div> | |||
<div class="item-Measurements-Units"> | |||
<p>%</p> | |||
<p>°C</p> | |||
<p>%</p> | |||
<p>Lux</p> | |||
</div> | |||
</div> | |||
<div mat-dialog-actions> | |||
<button mat-button mat-dialog-close>No</button> | |||
<button mat-button mat-dialog-close cdkFocusInitial (click)="onSaveDialog()">Save</button> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { ConfigurePlantDialogComponent } from './configure-plant-dialog.component'; | |||
// describe('ConfigurePlantDialogComponent', () => { | |||
// let component: ConfigurePlantDialogComponent; | |||
// let fixture: ComponentFixture<ConfigurePlantDialogComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ ConfigurePlantDialogComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(ConfigurePlantDialogComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,60 @@ | |||
import { Component, Inject, OnInit } from '@angular/core'; | |||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
import { ErrorDialogComponent } from '../error-dialog/error-dialog.component'; | |||
@Component({ | |||
selector: 'app-configure-plant-dialog', | |||
templateUrl: './configure-plant-dialog.component.html', | |||
styleUrls: ['./configure-plant-dialog.component.css'] | |||
}) | |||
export class ConfigurePlantDialogComponent implements OnInit { | |||
plant: Plant; | |||
constructor(public dialogRef: MatDialogRef<ConfigurePlantDialogComponent>, | |||
@Inject(MAT_DIALOG_DATA) public data: any, | |||
private mqttRequestService: MqttRequestService, | |||
public dialog: MatDialog) { | |||
this.plant = data.plant; | |||
} | |||
ngOnInit(): void { | |||
} | |||
onSaveDialog() { | |||
this.plant.AirTemperature = +(<HTMLInputElement>document.getElementById("inputAirTemperature")).value; | |||
this.plant.AirHumidity = +(<HTMLInputElement>document.getElementById("inputAirHumidity")).value; | |||
this.plant.SoilMoisture = +(<HTMLInputElement>document.getElementById("inputSoilMoisture")).value; | |||
this.plant.Brightness = +(<HTMLInputElement>document.getElementById("inputBrightness")).value; | |||
const plantInputString: string = this.plantInputTestFails(); | |||
if (plantInputString) { | |||
this.dialog.open(ErrorDialogComponent, { | |||
data: "Error: " + plantInputString, | |||
width: '400px', | |||
}); | |||
} else { | |||
//Request to the Backend, to change the data of the plant | |||
this.mqttRequestService.publishToCONFIGUREPLANT(this.plant); | |||
this.mqttRequestService.publishToGETALLDATA(); | |||
window.location.reload(); | |||
} | |||
} | |||
plantInputTestFails(): string { | |||
if (this.plant.AirTemperature > 100) { | |||
return "Air Temperature is to high"; | |||
} | |||
if (this.plant.AirHumidity > 100) { | |||
return "Air Humidity is to high"; | |||
} | |||
if (this.plant.SoilMoisture > 100) { | |||
return "Soil Moisture is to high"; | |||
} | |||
if (this.plant.Brightness > 100) { | |||
return "Brightness is to high"; | |||
} | |||
return ""; //Empty string is a Boolean false! | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
:host ::ng-deep { | |||
.mat-dialog-title { | |||
color: red; | |||
} | |||
.mat-button { | |||
background-color: ghostwhite; | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
<div class="container"> | |||
<p mat-dialog-title>Delete Plant: {{plant.PlantName}}</p> | |||
<p mat-dialog-content>Soll die Pflanze "{{plant.PlantName}}" tatsächlich gelöscht werden?</p> | |||
<div mat-dialog-actions> | |||
<button mat-button mat-dialog-close>No</button> | |||
<button mat-button mat-dialog-close cdkFocusInitial (click)="onDelete()" color="warn">Delete</button> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { DeleteConfirmDialogComponent } from './delete-confirm-dialog.component'; | |||
// describe('DeleteConfirmDialogComponent', () => { | |||
// let component: DeleteConfirmDialogComponent; | |||
// let fixture: ComponentFixture<DeleteConfirmDialogComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ DeleteConfirmDialogComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(DeleteConfirmDialogComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,24 @@ | |||
import { Component, Inject } from '@angular/core'; | |||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | |||
import { MqttRequestService } from 'src/app/Service/Mqtt/mqtt-request.service'; | |||
import { Plant } from 'src/app/models/plant.model'; | |||
import { ConfigurePlantDialogComponent } from '../configure-plant-dialog/configure-plant-dialog.component'; | |||
@Component({ | |||
selector: 'app-delete-confirm-dialog', | |||
templateUrl: './delete-confirm-dialog.component.html', | |||
styleUrls: ['./delete-confirm-dialog.component.css'] | |||
}) | |||
export class DeleteConfirmDialogComponent { | |||
plant: Plant; | |||
constructor(public dialogRef: MatDialogRef<ConfigurePlantDialogComponent>, | |||
@Inject(MAT_DIALOG_DATA) public data: Plant, private mqttRequestService: MqttRequestService) { | |||
this.plant = data; | |||
} | |||
onDelete() { | |||
this.mqttRequestService.publishToDELETEPLANT(this.plant.PlantID); | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
:host ::ng-deep { | |||
.mat-dialog-title { | |||
color: red; | |||
} | |||
.mat-button { | |||
background-color: ghostwhite; | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
<div class="container"> | |||
<p mat-dialog-title>Error</p> | |||
<p mat-dialog-content>{{text}}</p> | |||
<div mat-dialog-actions> | |||
<button mat-button mat-dialog-close (click)="onClose()">Close</button> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { ErrorDialogComponent } from './error-dialog.component'; | |||
// describe('ErrorDialogComponent', () => { | |||
// let component: ErrorDialogComponent; | |||
// let fixture: ComponentFixture<ErrorDialogComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ ErrorDialogComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(ErrorDialogComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,25 @@ | |||
import { Component, Inject, OnInit } from '@angular/core'; | |||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | |||
import { ConfigurePlantDialogComponent } from '../configure-plant-dialog/configure-plant-dialog.component'; | |||
@Component({ | |||
selector: 'app-error-dialog', | |||
templateUrl: './error-dialog.component.html', | |||
styleUrls: ['./error-dialog.component.css'] | |||
}) | |||
export class ErrorDialogComponent implements OnInit { | |||
text: string = ""; | |||
constructor(public dialogRef: MatDialogRef<ConfigurePlantDialogComponent>, | |||
@Inject(MAT_DIALOG_DATA) public data: any) { | |||
this.text = data; | |||
} | |||
ngOnInit(): void { | |||
} | |||
onClose() { | |||
window.location.reload(); | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
.container { | |||
width: 18.75rem; | |||
height: 21.875rem; | |||
display: grid; | |||
grid-template-rows: 15% 70% 15%; | |||
justify-content: start; | |||
align-content: start; | |||
} | |||
.item-header { | |||
grid-row: 1/2; | |||
justify-self: center; | |||
font-size: 1.6rem; | |||
color: red; | |||
} | |||
.item-img { | |||
grid-row: 2/3; | |||
justify-self: center; | |||
width: 21.875rem; | |||
height: 15.625rem; | |||
} | |||
.item-button { | |||
grid-row: 3/4; | |||
justify-self: center; | |||
} |
@@ -0,0 +1,7 @@ | |||
<div class="container"> | |||
<p class="item-header" mat-dialog-title>Map</p> | |||
<img class="item-img" src="assets/images/gruppe1.jpg" alt="not available"> | |||
<div class="item-button" mat-dialog-actions> | |||
<button mat-button mat-dialog-close>Close</button> | |||
</div> | |||
</div> |
@@ -0,0 +1,23 @@ | |||
// import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
// import { MapPlantDialogComponent } from './map-plant-dialog.component'; | |||
// describe('MapPlantDialogComponent', () => { | |||
// let component: MapPlantDialogComponent; | |||
// let fixture: ComponentFixture<MapPlantDialogComponent>; | |||
// beforeEach(async () => { | |||
// await TestBed.configureTestingModule({ | |||
// declarations: [ MapPlantDialogComponent ] | |||
// }) | |||
// .compileComponents(); | |||
// fixture = TestBed.createComponent(MapPlantDialogComponent); | |||
// component = fixture.componentInstance; | |||
// fixture.detectChanges(); | |||
// }); | |||
// it('should create', () => { | |||
// expect(component).toBeTruthy(); | |||
// }); | |||
// }); |
@@ -0,0 +1,15 @@ | |||
import { Component, OnInit } from '@angular/core'; | |||
@Component({ | |||
selector: 'app-map-plant-dialog', | |||
templateUrl: './map-plant-dialog.component.html', | |||
styleUrls: ['./map-plant-dialog.component.css'] | |||
}) | |||
export class MapPlantDialogComponent implements OnInit { | |||
constructor() { } | |||
ngOnInit(): void { | |||
} | |||
} |
@@ -0,0 +1,10 @@ | |||
export interface Plant { | |||
AirTemperature: number; | |||
AirHumidity: number; | |||
SoilMoisture: number; | |||
Brightness: number; | |||
PlantID: number; | |||
Timestamp: string; | |||
MeasurementID: string; | |||
PlantName: string; | |||
} |
@@ -0,0 +1,4 @@ | |||
export interface PlantCount { | |||
CurrentCount: number; | |||
MaxCount: number; | |||
} |
@@ -0,0 +1,4 @@ | |||
export interface Position { | |||
Position: string, | |||
Timestamp: string | |||
} |
@@ -0,0 +1,4 @@ | |||
export interface RobotBattery { | |||
Battery: number; | |||
Timestamp: string; | |||
} |
@@ -0,0 +1,35 @@ | |||
// Custom Theming for Angular Material | |||
// For more information: https://material.angular.io/guide/theming | |||
@use '@angular/material' as mat; | |||
// Plus imports for other components in your app. | |||
// Include the common styles for Angular Material. We include this here so that you only | |||
// have to load a single css file for Angular Material in your app. | |||
// Be sure that you only ever include this mixin once! | |||
@include mat.core(); | |||
// Define the palettes for your theme using the Material Design palettes available in palette.scss | |||
// (imported above). For each palette, you can optionally specify a default, lighter, and darker | |||
// hue. Available color palettes: https://material.io/design/color/ | |||
$Frontend-primary: mat.define-palette(mat.$indigo-palette); | |||
$Frontend-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); | |||
// The warn palette is optional (defaults to red). | |||
$Frontend-warn: mat.define-palette(mat.$red-palette); | |||
// Create the theme object. A theme consists of configurations for individual | |||
// theming systems such as "color" or "typography". | |||
$Frontend-theme: mat.define-light-theme(( | |||
color: ( | |||
primary: $Frontend-primary, | |||
accent: $Frontend-accent, | |||
warn: $Frontend-warn, | |||
) | |||
)); | |||
// Include theme styles for core and each component used in your app. | |||
// Alternatively, you can import and @include the theme mixins for each component | |||
// that you are using. | |||
@include mat.all-component-themes($Frontend-theme); | |||
@@ -0,0 +1,3 @@ | |||
export const environment = { | |||
production: true | |||
}; |
@@ -0,0 +1,16 @@ | |||
// This file can be replaced during build by using the `fileReplacements` array. | |||
// `ng build` replaces `environment.ts` with `environment.prod.ts`. | |||
// The list of file replacements can be found in `angular.json`. | |||
export const environment = { | |||
production: false | |||
}; | |||
/* | |||
* For easier debugging in development mode, you can import the following file | |||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. | |||
* | |||
* This import should be commented out in production mode because it will have a negative impact | |||
* on performance if an error is thrown. | |||
*/ | |||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. |
@@ -0,0 +1,19 @@ | |||
<!doctype html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>Frontend</title> | |||
<base href="/"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |||
<link rel="icon" type="image/x-icon" href="favicon.ico"> | |||
<link rel="preconnect" href="https://fonts.gstatic.com"> | |||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> | |||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | |||
</head> | |||
<body class="mat-typography"> | |||
<app-root></app-root> | |||
</body> | |||
</html> |
@@ -0,0 +1,12 @@ | |||
import { enableProdMode } from '@angular/core'; | |||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |||
import { AppModule } from './app/app.module'; | |||
import { environment } from './environments/environment'; | |||
if (environment.production) { | |||
enableProdMode(); | |||
} | |||
platformBrowserDynamic().bootstrapModule(AppModule) | |||
.catch(err => console.error(err)); |
@@ -0,0 +1,53 @@ | |||
/** | |||
* This file includes polyfills needed by Angular and is loaded before the app. | |||
* You can add your own extra polyfills to this file. | |||
* | |||
* This file is divided into 2 sections: | |||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. | |||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main | |||
* file. | |||
* | |||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that | |||
* automatically update themselves. This includes recent versions of Safari, Chrome (including | |||
* Opera), Edge on the desktop, and iOS and Chrome on mobile. | |||
* | |||
* Learn more in https://angular.io/guide/browser-support | |||
*/ | |||
/*************************************************************************************************** | |||
* BROWSER POLYFILLS | |||
*/ | |||
/** | |||
* By default, zone.js will patch all possible macroTask and DomEvents | |||
* user can disable parts of macroTask/DomEvents patch by setting following flags | |||
* because those flags need to be set before `zone.js` being loaded, and webpack | |||
* will put import in the top of bundle, so user need to create a separate file | |||
* in this directory (for example: zone-flags.ts), and put the following flags | |||
* into that file, and then add the following code before importing zone.js. | |||
* import './zone-flags'; | |||
* | |||
* The flags allowed in zone-flags.ts are listed here. | |||
* | |||
* The following flags will work for all browsers. | |||
* | |||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame | |||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick | |||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames | |||
* | |||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js | |||
* with the following flag, it will bypass `zone.js` patch for IE/Edge | |||
* | |||
* (window as any).__Zone_enable_cross_context_check = true; | |||
* | |||
*/ | |||
/*************************************************************************************************** | |||
* Zone JS is required by default for Angular itself. | |||
*/ | |||
import 'zone.js'; // Included with Angular CLI. | |||
/*************************************************************************************************** | |||
* APPLICATION IMPORTS | |||
*/ |
@@ -0,0 +1,4 @@ | |||
/* You can add global styles to this file, and also import other style files */ | |||
html, body { height: 100%; } | |||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } |
@@ -0,0 +1,26 @@ | |||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files | |||
import 'zone.js/testing'; | |||
import { getTestBed } from '@angular/core/testing'; | |||
import { | |||
BrowserDynamicTestingModule, | |||
platformBrowserDynamicTesting | |||
} from '@angular/platform-browser-dynamic/testing'; | |||
declare const require: { | |||
context(path: string, deep?: boolean, filter?: RegExp): { | |||
<T>(id: string): T; | |||
keys(): string[]; | |||
}; | |||
}; | |||
// First, initialize the Angular testing environment. | |||
getTestBed().initTestEnvironment( | |||
BrowserDynamicTestingModule, | |||
platformBrowserDynamicTesting(), | |||
); | |||
// Then we find all the tests. | |||
const context = require.context('./', true, /\.spec\.ts$/); | |||
// And load the modules. | |||
context.keys().forEach(context); |
@@ -0,0 +1,15 @@ | |||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ | |||
{ | |||
"extends": "./tsconfig.json", | |||
"compilerOptions": { | |||
"outDir": "./out-tsc/app", | |||
"types": [] | |||
}, | |||
"files": [ | |||
"src/main.ts", | |||
"src/polyfills.ts" | |||
], | |||
"include": [ | |||
"src/**/*.d.ts" | |||
] | |||
} |