Plot
This commit is contained in:
parent
9d9bf75dd6
commit
2087d5e64d
61
src/main/angular/package-lock.json
generated
61
src/main/angular/package-lock.json
generated
@ -14,6 +14,9 @@
|
|||||||
"@angular/forms": "^20.3.0",
|
"@angular/forms": "^20.3.0",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "^20.3.0",
|
||||||
"@angular/router": "^20.3.0",
|
"@angular/router": "^20.3.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^3.0.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@stomp/ng2-stompjs": "^8.0.0",
|
"@stomp/ng2-stompjs": "^8.0.0",
|
||||||
"@stomp/stompjs": "^7.2.0",
|
"@stomp/stompjs": "^7.2.0",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
@ -1397,6 +1400,64 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/angular-fontawesome": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-+8Dd6DoJnqArfrZ5NvjHyRL64IIkTigXclbOOcFdYQ8/WFERQUDaEU6SAV8Q0JBpJhMS1McED7YCOCAE6SIVyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": "^20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-x0cR55ILVqFpUioSMf6ebpRCMXMcheGN743P05W2RB5uCNpJUqWIqW66Lap8PfL/lngvjTbZj0BNSUweIr/fHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-4V9fHbHjcx9Qu4O99AM5B4zuEDfB4zajk1I77hEzOxPN00f8g3484Aeq6WpfFcmookvjLE3Pr71Dhf/lqw7tbA==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==",
|
||||||
|
"license": "(CC-BY-4.0 AND MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@inquirer/ansi": {
|
"node_modules/@inquirer/ansi": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz",
|
||||||
|
|||||||
@ -28,6 +28,9 @@
|
|||||||
"@angular/forms": "^20.3.0",
|
"@angular/forms": "^20.3.0",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "^20.3.0",
|
||||||
"@angular/router": "^20.3.0",
|
"@angular/router": "^20.3.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^3.0.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@stomp/ng2-stompjs": "^8.0.0",
|
"@stomp/ng2-stompjs": "^8.0.0",
|
||||||
"@stomp/stompjs": "^7.2.0",
|
"@stomp/stompjs": "^7.2.0",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {Injectable} from '@angular/core';
|
|||||||
import {RxStompState} from '@stomp/rx-stomp';
|
import {RxStompState} from '@stomp/rx-stomp';
|
||||||
|
|
||||||
export type FromJson<T> = (json: any) => T;
|
export type FromJson<T> = (json: any) => T;
|
||||||
|
export type FromJsonIndexed<T> = (json: any, index: number) => T;
|
||||||
|
|
||||||
export type Next<T> = (item: T) => any;
|
export type Next<T> = (item: T) => any;
|
||||||
|
|
||||||
@ -52,6 +53,10 @@ export function validateList<T>(json: any, fromJson: FromJson<T>): T[] {
|
|||||||
return json.map(fromJson);
|
return json.map(fromJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateListIndexed<T>(json: any, fromJson: FromJsonIndexed<T>): T[] {
|
||||||
|
return json.map(fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
export function url(protocol: string, path: any[]): string {
|
export function url(protocol: string, path: any[]): string {
|
||||||
const secure = location.protocol.endsWith('s:') ? 's' : '';
|
const secure = location.protocol.endsWith('s:') ? 's' : '';
|
||||||
return `${protocol}${secure}://${location.hostname}:8080/${path.join('/')}`;
|
return `${protocol}${secure}://${location.hostname}:8080/${path.join('/')}`;
|
||||||
@ -116,19 +121,19 @@ export class ApiService {
|
|||||||
return this._websocketError;
|
return this._websocketError;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSingle<T>(path: any[], fromJson: FromJson<T>, next: Next<T>): void {
|
getSingle<T>(path: any[], fromJson: FromJson<T>, next?: Next<T>): void {
|
||||||
this.http.get<any>(url('http', path)).pipe(map(fromJson)).subscribe(next);
|
this.http.get<any>(url('http', path)).pipe(map(fromJson)).subscribe(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
getList<T>(path: any[], fromJson: FromJson<T>, next: Next<T[]>): void {
|
getList<T>(path: any[], fromJson: FromJson<T>, next?: Next<T[]>): void {
|
||||||
this.http.get<any[]>(url('http', path)).pipe(map(list => list.map(fromJson))).subscribe(next);
|
this.http.get<any[]>(url('http', path)).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T>): void {
|
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): void {
|
||||||
this.http.post<any>(url('http', path), data).pipe(map(fromJson)).subscribe(next);
|
this.http.post<any>(url('http', path), data).pipe(map(fromJson)).subscribe(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
postList<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T[]>): void {
|
postList<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T[]>): void {
|
||||||
this.http.post<any[]>(url('http', path), data).pipe(map(list => list.map(fromJson))).subscribe(next);
|
this.http.post<any[]>(url('http', path), data).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +147,7 @@ export class ApiService {
|
|||||||
return this.stompService.connectionState$.pipe(filter(state => state !== RxStompState.OPEN)).subscribe(_ => next());
|
return this.stompService.connectionState$.pipe(filter(state => state !== RxStompState.OPEN)).subscribe(_ => next());
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe<T>(topic: any[], fromJson: FromJson<T>, next: Next<T>): Subscription {
|
subscribe<T>(topic: any[], fromJson: FromJson<T>, next?: Next<T>): Subscription {
|
||||||
return this.stompService
|
return this.stompService
|
||||||
.subscribe(topic.join("/"))
|
.subscribe(topic.join("/"))
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -169,22 +174,36 @@ export abstract class CrudService<T> {
|
|||||||
this.getList(["findAll"], next);
|
this.getList(["findAll"], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getSingle(path: any[], next: Next<T>) {
|
protected getSingle(path: any[], next?: Next<T>) {
|
||||||
this.api.getSingle<T>([...this.path, ...path], this.fromJson, next);
|
this.api.getSingle<T>([...this.path, ...path], this.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getList(path: any[], next: Next<T[]>) {
|
protected getList(path: any[], next?: Next<T[]>) {
|
||||||
this.api.getList<T>([...this.path, ...path], this.fromJson, next);
|
this.api.getList<T>([...this.path, ...path], this.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected postSingle(path: any[], data: any, next: Next<T>) {
|
protected postSingle(path: any[], data: any, next?: Next<T>) {
|
||||||
this.api.postSingle<T>([...this.path, ...path], data, this.fromJson, next);
|
this.api.postSingle<T>([...this.path, ...path], data, this.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(next: Next<T>): Subscription {
|
subscribe(next: Next<T>, path: any[] = []): Subscription {
|
||||||
const subs: Subscription[] = [];
|
const subs: Subscription[] = [];
|
||||||
subs.push(this.api.subscribe([...this.path], this.fromJson, next));
|
subs.push(this.api.subscribe([...this.path, ...path], this.fromJson, next));
|
||||||
return new Subscription(() => subs.forEach(sub => sub.unsubscribe()));
|
return new Subscription(() => subs.forEach(sub => sub.unsubscribe()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NTU<T>(v: T | null | undefined): T | undefined {
|
||||||
|
if (v === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UTN<T>(v: T | null | undefined): T | null {
|
||||||
|
if (v === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,11 @@ import {routes} from './app.routes';
|
|||||||
import {provideHttpClient} from '@angular/common/http';
|
import {provideHttpClient} from '@angular/common/http';
|
||||||
import {stompServiceFactory} from './COMMON';
|
import {stompServiceFactory} from './COMMON';
|
||||||
import {StompService} from '@stomp/ng2-stompjs';
|
import {StompService} from '@stomp/ng2-stompjs';
|
||||||
|
import {registerLocaleData} from '@angular/common';
|
||||||
|
import localeDe from '@angular/common/locales/de';
|
||||||
|
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||||
|
|
||||||
|
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
<app-plot></app-plot>
|
|
||||||
|
|
||||||
<router-outlet/>
|
<router-outlet/>
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
|
import {PlotComponent} from './plot/plot.component';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{path: 'plot', component: PlotComponent},
|
||||||
|
{path: 'plot/:id', component: PlotComponent},
|
||||||
|
{path: '**', redirectTo: 'plot'},
|
||||||
|
];
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import {Component, signal} from '@angular/core';
|
import {Component, signal} from '@angular/core';
|
||||||
import {RouterOutlet} from '@angular/router';
|
import {RouterOutlet} from '@angular/router';
|
||||||
import {Plot} from './plot/plot';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, Plot],
|
imports: [RouterOutlet],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.less'
|
styleUrl: './app.less'
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
<input type="checkbox" [(ngModel)]="model" (change)="apply()">
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkbox',
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './checkbox.component.html',
|
||||||
|
styleUrl: './checkbox.component.less'
|
||||||
|
})
|
||||||
|
export class CheckboxComponent {
|
||||||
|
|
||||||
|
protected _initial: boolean = false;
|
||||||
|
|
||||||
|
protected model: boolean = false;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly onChange = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set initial(v: boolean) {
|
||||||
|
this._initial = v;
|
||||||
|
this.model = this._initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
if (this.model !== this._initial) {
|
||||||
|
this.onChange.emit(this.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<input type="number" [(ngModel)]="model" (focus)="focus()" (blur)="apply()" (change)="!edit && apply()" (keydown.enter)="apply()" (keydown.esc)="cancel()">
|
||||||
53
src/main/angular/src/app/common/number/number.component.ts
Normal file
53
src/main/angular/src/app/common/number/number.component.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-number',
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './number.component.html',
|
||||||
|
styleUrl: './number.component.less'
|
||||||
|
})
|
||||||
|
export class NumberComponent {
|
||||||
|
|
||||||
|
protected _initial: number | null = null;
|
||||||
|
|
||||||
|
protected edit: boolean = false;
|
||||||
|
|
||||||
|
protected model: string = "";
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly onChange = new EventEmitter<number | null>();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set initial(v: number | null) {
|
||||||
|
this._initial = v;
|
||||||
|
if (!this.edit) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.edit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
this.edit = false;
|
||||||
|
const value = parseFloat(this.model);
|
||||||
|
const value1 = isNaN(value) ? null : value;
|
||||||
|
if (value1 !== this._initial) {
|
||||||
|
this.onChange.emit(value1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.edit = false;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset() {
|
||||||
|
this.model = this._initial === null ? "" : this._initial + "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<input type="number" [(ngModel)]="model" (focus)="focus()" (blur)="apply()" (change)="!edit && apply()" (keydown.enter)="apply()" (keydown.esc)="cancel()">
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-number-nn',
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './number-n-n.component.html',
|
||||||
|
styleUrl: './number-n-n.component.less'
|
||||||
|
})
|
||||||
|
export class NumberNNComponent {
|
||||||
|
|
||||||
|
protected _initial!: number;
|
||||||
|
|
||||||
|
protected edit: boolean = false;
|
||||||
|
|
||||||
|
protected model: string = "";
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly onChange = new EventEmitter<number>();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set initial(v: number) {
|
||||||
|
this._initial = v;
|
||||||
|
if (!this.edit) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.edit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
this.edit = false;
|
||||||
|
const value = parseFloat(this.model);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
this.reset();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value != this._initial) {
|
||||||
|
this.onChange.emit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.edit = false;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset() {
|
||||||
|
this.model = this._initial === null ? "" : this._initial + "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
src/main/angular/src/app/common/text/text.component.html
Normal file
1
src/main/angular/src/app/common/text/text.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<input type="text" [(ngModel)]="model" (focus)="focus()" (blur)="apply()" (keydown.enter)="apply()" (keydown.esc)="cancel()">
|
||||||
51
src/main/angular/src/app/common/text/text.component.ts
Normal file
51
src/main/angular/src/app/common/text/text.component.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-text',
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './text.component.html',
|
||||||
|
styleUrl: './text.component.less'
|
||||||
|
})
|
||||||
|
export class TextComponent {
|
||||||
|
|
||||||
|
protected _initial!: string;
|
||||||
|
|
||||||
|
private edit: boolean = false;
|
||||||
|
|
||||||
|
protected model: string = "";
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly onChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set initial(v: string) {
|
||||||
|
this._initial = v;
|
||||||
|
if (!this.edit) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.edit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
this.edit = false;
|
||||||
|
if (this.model !== this._initial) {
|
||||||
|
this.onChange.emit(this.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.edit = false;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset() {
|
||||||
|
this.model = this._initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
src/main/angular/src/app/plot/Axis.ts
Normal file
45
src/main/angular/src/app/plot/Axis.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {mapNotNull, validateBoolean, validateList, validateNumber, validateString} from "../COMMON";
|
||||||
|
import {Graph} from './Graph';
|
||||||
|
import {Plot} from './Plot';
|
||||||
|
|
||||||
|
export class Axis {
|
||||||
|
|
||||||
|
readonly graphs: Graph[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly plot: Plot,
|
||||||
|
readonly id: number,
|
||||||
|
readonly version: number,
|
||||||
|
readonly index: number,
|
||||||
|
readonly name: string,
|
||||||
|
readonly unit: string,
|
||||||
|
readonly visible: boolean,
|
||||||
|
readonly right: boolean,
|
||||||
|
readonly min: number | null,
|
||||||
|
readonly minHard: boolean,
|
||||||
|
readonly max: number | null,
|
||||||
|
readonly maxHard: boolean,
|
||||||
|
graphs: any[],
|
||||||
|
) {
|
||||||
|
this.graphs = validateList(graphs, json => Graph.fromJson(this, json))
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(plot: Plot, index: number, json: any): Axis {
|
||||||
|
return new Axis(
|
||||||
|
plot,
|
||||||
|
validateNumber(json.id),
|
||||||
|
validateNumber(json.version),
|
||||||
|
index,
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.unit),
|
||||||
|
validateBoolean(json.visible),
|
||||||
|
validateBoolean(json.right),
|
||||||
|
mapNotNull(json.min, validateNumber),
|
||||||
|
validateBoolean(json.minHard),
|
||||||
|
mapNotNull(json.max, validateNumber),
|
||||||
|
validateBoolean(json.maxHard),
|
||||||
|
json.graphs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/main/angular/src/app/plot/Graph.ts
Normal file
49
src/main/angular/src/app/plot/Graph.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {Series} from "../series/Series";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {validateBoolean, validateNumber, validateString} from "../COMMON";
|
||||||
|
import {Axis} from './Axis';
|
||||||
|
import {SeriesType} from '../series/SeriesType';
|
||||||
|
|
||||||
|
export class Graph {
|
||||||
|
|
||||||
|
readonly type: string = "line";
|
||||||
|
|
||||||
|
readonly mid: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly axis: Axis,
|
||||||
|
readonly id: number,
|
||||||
|
readonly version: number,
|
||||||
|
readonly series: Series,
|
||||||
|
readonly name: string,
|
||||||
|
readonly visible: boolean,
|
||||||
|
readonly color: string,
|
||||||
|
readonly factor: number,
|
||||||
|
readonly group: Group,
|
||||||
|
readonly stack: string,
|
||||||
|
readonly min: boolean,
|
||||||
|
readonly max: boolean,
|
||||||
|
readonly avg: boolean,
|
||||||
|
) {
|
||||||
|
this.mid = this.avg || this.series.type === SeriesType.BOOL || this.series.type === SeriesType.DELTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(axis: Axis, json: any): Graph {
|
||||||
|
return new Graph(
|
||||||
|
axis,
|
||||||
|
validateNumber(json.id),
|
||||||
|
validateNumber(json.version),
|
||||||
|
Series.fromJson(json.series),
|
||||||
|
validateString(json.name),
|
||||||
|
validateBoolean(json.visible),
|
||||||
|
validateString(json.color),
|
||||||
|
validateNumber(json.factor),
|
||||||
|
validateString(json.group) as Group,
|
||||||
|
validateString(json.stack),
|
||||||
|
validateBoolean(json.min),
|
||||||
|
validateBoolean(json.max),
|
||||||
|
validateBoolean(json.avg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/main/angular/src/app/plot/Group.ts
Normal file
11
src/main/angular/src/app/plot/Group.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export enum Group {
|
||||||
|
NONE = "NONE",
|
||||||
|
FIVE_OF_DAY = "FIVE_OF_DAY",
|
||||||
|
HOUR_OF_DAY = "HOUR_OF_DAY",
|
||||||
|
HOUR_OF_WEEK = "HOUR_OF_WEEK",
|
||||||
|
HOUR_OF_MONTH = "HOUR_OF_MONTH",
|
||||||
|
DAY_OF_WEEK = "DAY_OF_WEEK",
|
||||||
|
DAY_OF_MONTH = "DAY_OF_MONTH",
|
||||||
|
DAY_OF_YEAR = "DAY_OF_YEAR",
|
||||||
|
WEEK_OF_YEAR = "WEEK_OF_YEAR",
|
||||||
|
}
|
||||||
43
src/main/angular/src/app/plot/Plot.ts
Normal file
43
src/main/angular/src/app/plot/Plot.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {validateBoolean, validateListIndexed, validateNumber, validateString} from '../COMMON';
|
||||||
|
import {Interval} from '../series/Interval';
|
||||||
|
import {Axis} from './Axis';
|
||||||
|
|
||||||
|
export class Plot {
|
||||||
|
|
||||||
|
readonly axes: Axis[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly id: number,
|
||||||
|
readonly version: number,
|
||||||
|
readonly deleted: boolean,
|
||||||
|
readonly name: string,
|
||||||
|
readonly interval: Interval,
|
||||||
|
readonly offset: number,
|
||||||
|
readonly duration: number,
|
||||||
|
readonly dashboard: boolean,
|
||||||
|
readonly position: number,
|
||||||
|
axes: any[],
|
||||||
|
) {
|
||||||
|
this.axes = validateListIndexed(axes, ((json, index) => Axis.fromJson(this, index, json)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Plot {
|
||||||
|
return new Plot(
|
||||||
|
validateNumber(json.id),
|
||||||
|
validateNumber(json.version),
|
||||||
|
validateBoolean(json.deleted),
|
||||||
|
validateString(json.name),
|
||||||
|
Interval.fromJson(json.interval),
|
||||||
|
validateNumber(json.offset),
|
||||||
|
validateNumber(json.duration),
|
||||||
|
validateBoolean(json.dashboard),
|
||||||
|
validateNumber(json.position),
|
||||||
|
json.axes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static comparePosition(a: Plot, b: Plot) {
|
||||||
|
return a.position - b.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
220
src/main/angular/src/app/plot/plot.component.html
Normal file
220
src/main/angular/src/app/plot/plot.component.html
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<div class="layout">
|
||||||
|
<div class="header">
|
||||||
|
<select [ngModel]="plot" (ngModelChange)="buildPlot($event)">
|
||||||
|
@for (p of plotList; track p.id) {
|
||||||
|
<option [ngValue]="p">#{{ p.position }}: {{ p.name || '---' }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button (click)="plotService.plotCreate(updateAndBuild);">
|
||||||
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<button (click)="plot && plotService.plotDuplicate(plot, updateAndBuild)" [disabled]="!plot">
|
||||||
|
<fa-icon [icon]="faCopy"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<button (click)="plot && plotService.plotDelete(plot, updatePlot)" [disabled]="!plot">
|
||||||
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div #container class="container">
|
||||||
|
<canvas #chartCanvas></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (plot) {
|
||||||
|
|
||||||
|
<div class="Section PlotDetails">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Intervall</th>
|
||||||
|
<th>Verschiebung</th>
|
||||||
|
<th>Dauer</th>
|
||||||
|
<th class="vertical">Dash</th>
|
||||||
|
<th>Position</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="plot.name" (onChange)="plotService.plotName(plot, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select [ngModel]="plot.interval" (ngModelChange)="plotService.plotInterval(plot, $event, updatePlot)">
|
||||||
|
@for (interval of Interval.values; track interval) {
|
||||||
|
<option [ngValue]="interval">{{ interval.display }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number-nn [initial]="plot.offset" (onChange)="plotService.plotOffset(plot, $event, updatePlot)"></app-number-nn>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number-nn [initial]="plot.duration" (onChange)="plotService.plotDuration(plot, $event, updatePlot)"></app-number-nn>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="plot.dashboard" (onChange)="plotService.plotDashboard(plot, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number-nn [initial]="plot.position" (onChange)="plotService.plotPosition(plot, $event, updatePlot)"></app-number-nn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="Section Axes">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<fa-icon [icon]="faEye"></fa-icon>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<fa-icon [icon]="faListAlt"></fa-icon>
|
||||||
|
</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Einheit</th>
|
||||||
|
<th class="vertical">rechts</th>
|
||||||
|
<th>min</th>
|
||||||
|
<th class="vertical">fest</th>
|
||||||
|
<th>max</th>
|
||||||
|
<th class="vertical">fest</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
@for (axis of plot.axes; track axis.id) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="axis.visible" (onChange)="plotService.axisVisible(axis, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Y{{ axis.index + 1 }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="axis.name" (onChange)="plotService.axisName(axis, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="axis.unit" (onChange)="plotService.axisUnit(axis, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="axis.right" (onChange)="plotService.axisRight(axis, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number [initial]="axis.min" (onChange)="plotService.axisMin(axis, $event, updatePlot)"></app-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="axis.minHard" (onChange)="plotService.axisMinHard(axis, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number [initial]="axis.max" (onChange)="plotService.axisMax(axis, $event, updatePlot)"></app-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="axis.maxHard" (onChange)="plotService.axisMaxHard(axis, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button (click)="plotService.axisDelete(axis, updatePlot)">
|
||||||
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
|
Achse
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="plotService.axisAddGraph(axis, updatePlot)">
|
||||||
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
|
Grafen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button (click)="plotService.plotAddAxis(plot, updatePlot)">
|
||||||
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
|
Achse
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="Section Graphs">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<fa-icon [icon]="faEye"></fa-icon>
|
||||||
|
</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Serie</th>
|
||||||
|
<th>Farbe</th>
|
||||||
|
<th>Faktor</th>
|
||||||
|
<th>Aggregat</th>
|
||||||
|
<th>Stack</th>
|
||||||
|
<th class="vertical">min</th>
|
||||||
|
<th>∅</th>
|
||||||
|
<th class="vertical">max</th>
|
||||||
|
<th>Achse</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
@for (axis of plot.axes; track axis.id) {
|
||||||
|
@for (graph of axis.graphs; track graph.id) {
|
||||||
|
<tr [style.background-color]="graph.color">
|
||||||
|
<td>
|
||||||
|
<app-checkbox [initial]="graph.visible" (onChange)="plotService.graphVisible(graph, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="graph.name" (onChange)="plotService.graphName(graph, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select [ngModel]="graph.series.id" (ngModelChange)="plotService.graphSeries(graph, $event, updatePlot)">
|
||||||
|
@for (s of seriesList; track s.id) {
|
||||||
|
<option [ngValue]="s.id">{{ s.name }} [{{ s.valueString }}]</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="graph.color" (onChange)="plotService.graphColor(graph, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-number-nn [initial]="graph.factor" (onChange)="plotService.graphFactor(graph, $event, updatePlot)"></app-number-nn>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select [ngModel]="graph.group" (ngModelChange)="plotService.graphGroup(graph, $event, updatePlot)">
|
||||||
|
@for (group of groups(); track group) {
|
||||||
|
<option [ngValue]="group">{{ group }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<app-text [initial]="graph.stack" (onChange)="plotService.graphStack(graph, $event, updatePlot)"></app-text>
|
||||||
|
</td>
|
||||||
|
@if (graph.series.type === SeriesType.VARYING) {
|
||||||
|
<td class="subSeries">
|
||||||
|
<app-checkbox [initial]="graph.min" (onChange)="plotService.graphMin(graph, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td class="subSeries">
|
||||||
|
<app-checkbox [initial]="graph.avg" (onChange)="plotService.graphAvg(graph, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
<td class="subSeries">
|
||||||
|
<app-checkbox [initial]="graph.max" (onChange)="plotService.graphMax(graph, $event, updatePlot)"></app-checkbox>
|
||||||
|
</td>
|
||||||
|
} @else {
|
||||||
|
@if (graph.series.type === SeriesType.BOOL) {
|
||||||
|
<td colspan="3" class="subSeries">Boolean</td>
|
||||||
|
} @else {
|
||||||
|
<td colspan="3" class="subSeries">Delta</td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<td>
|
||||||
|
<select [ngModel]="graph.axis.id" (ngModelChange)="plotService.graphAxis(graph, $event, updatePlot)">
|
||||||
|
@for (axis of plot.axes; track axis.id) {
|
||||||
|
<option [ngValue]="axis.id">Y{{ axis.index + 1 }} {{ axis.name }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button (click)="plotService.graphDelete(graph, updatePlot)">
|
||||||
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
43
src/main/angular/src/app/plot/plot.component.less
Normal file
43
src/main/angular/src/app/plot/plot.component.less
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subSeries {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
rotate: 180deg;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Section {
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.PlotDetails {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.Axes {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Graphs {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
374
src/main/angular/src/app/plot/plot.component.ts
Normal file
374
src/main/angular/src/app/plot/plot.component.ts
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||||
|
import {BarController, BarElement, CategoryScale, Chart, ChartDataset, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
||||||
|
import {SeriesService} from "../series/series.service";
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import {toAvg, toBool, toDelta, toMax, toMin} from '../series/MinMaxAvg';
|
||||||
|
import {SeriesType} from '../series/SeriesType';
|
||||||
|
import {PlotService} from './plot.service';
|
||||||
|
import {Plot} from './Plot';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {NTU, UTN} from '../COMMON';
|
||||||
|
import {Graph} from './Graph';
|
||||||
|
import {Axis} from './Axis';
|
||||||
|
import {Series} from '../series/Series';
|
||||||
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {faCopy, faEye, faListAlt} from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import {TextComponent} from '../common/text/text.component';
|
||||||
|
import {NumberComponent} from '../common/number/number.component';
|
||||||
|
import {CheckboxComponent} from '../common/checkbox/checkbox.component';
|
||||||
|
import {Group} from './Group';
|
||||||
|
import {NumberNNComponent} from '../common/numberNN/number-n-n.component';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {Delta, DeltaService} from '../series/delta/delta-service';
|
||||||
|
import {Bool, BoolService} from '../series/bool/bool-service';
|
||||||
|
import {Varying, VaryingService} from '../series/varying/varying-service';
|
||||||
|
import {Interval} from '../series/Interval';
|
||||||
|
import {faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
import {Location} from '@angular/common';
|
||||||
|
|
||||||
|
type Dataset = ChartDataset<any, any>[][number];
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
TimeScale,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function unitInBrackets(unit: string): string {
|
||||||
|
if (!unit) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return ` [${unit}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-plot',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
FaIconComponent,
|
||||||
|
TextComponent,
|
||||||
|
NumberComponent,
|
||||||
|
CheckboxComponent,
|
||||||
|
NumberNNComponent
|
||||||
|
],
|
||||||
|
templateUrl: './plot.component.html',
|
||||||
|
styleUrl: './plot.component.less'
|
||||||
|
})
|
||||||
|
export class PlotComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly SeriesType = SeriesType;
|
||||||
|
|
||||||
|
protected readonly Interval = Interval;
|
||||||
|
|
||||||
|
protected readonly faEye = faEye;
|
||||||
|
|
||||||
|
protected readonly faListAlt = faListAlt;
|
||||||
|
|
||||||
|
protected readonly faCopy = faCopy;
|
||||||
|
|
||||||
|
protected readonly faPlus = faPlus;
|
||||||
|
|
||||||
|
protected readonly faTrash = faTrash;
|
||||||
|
|
||||||
|
@ViewChild('chartCanvas')
|
||||||
|
canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
@ViewChild('container')
|
||||||
|
chartContainer!: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
private chart!: Chart;
|
||||||
|
|
||||||
|
protected seriesList: Series[] = [];
|
||||||
|
|
||||||
|
protected plotList: Plot[] = [];
|
||||||
|
|
||||||
|
protected plot: Plot | null = null;
|
||||||
|
|
||||||
|
private readonly subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
private readonly graphSubscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly serieService: SeriesService,
|
||||||
|
readonly plotService: PlotService,
|
||||||
|
readonly boolService: BoolService,
|
||||||
|
readonly deltaService: DeltaService,
|
||||||
|
readonly varyingService: VaryingService,
|
||||||
|
readonly activatedRoute: ActivatedRoute,
|
||||||
|
readonly location: Location
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
this.chart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: [],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
position: 'nearest',
|
||||||
|
mode: 'x',
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.serieService.findAll(series => {
|
||||||
|
this.seriesList = series.sort(Series.compareName);
|
||||||
|
this.plotService.findAll(plots => {
|
||||||
|
this.plotList = plots.sort(Plot.comparePosition);
|
||||||
|
this.subscriptions.push(this.plotService.subscribe(this.updatePlot));
|
||||||
|
this.activatedRoute.params.subscribe(params => {
|
||||||
|
const id = parseInt(params["id"]);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
this.buildPlot(this.plotList[0]);
|
||||||
|
} else {
|
||||||
|
this.buildPlot(this.plotList.filter(plot => plot.id === id)[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.subscriptions.push(this.serieService.subscribe(this.updateSeries));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly buildPlot = (plot: Plot | null | undefined): void => {
|
||||||
|
this.plot = UTN(plot);
|
||||||
|
|
||||||
|
this.graphSubscriptions.forEach(s => s.unsubscribe());
|
||||||
|
this.graphSubscriptions.length = 0;
|
||||||
|
for (const key in this.chart.options.scales) {
|
||||||
|
if (key.startsWith("y")) {
|
||||||
|
delete this.chart.options.scales[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.chart.data.labels = [];
|
||||||
|
this.chart.data.datasets = [];
|
||||||
|
this.chart.update();
|
||||||
|
|
||||||
|
if (!this.plot) {
|
||||||
|
this.location.go('/plot');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.location.go('/plot/' + this.plot.id);
|
||||||
|
if (!this.chart.options.plugins) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.chart.options.plugins.title = {
|
||||||
|
display: !!this.plot.name,
|
||||||
|
text: this.plot.name,
|
||||||
|
};
|
||||||
|
for (const axis of this.plot.axes) {
|
||||||
|
this.buildAxis(axis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private buildAxis(axis: Axis): void {
|
||||||
|
if (!this.chart.options.scales) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = axis.name + unitInBrackets(axis.unit);
|
||||||
|
this.chart.options.scales["y" + axis.id] = {
|
||||||
|
display: axis.visible,
|
||||||
|
title: {
|
||||||
|
display: !!name,
|
||||||
|
text: name,
|
||||||
|
},
|
||||||
|
suggestedMin: NTU(axis.min),
|
||||||
|
suggestedMax: NTU(axis.max),
|
||||||
|
min: axis.minHard ? NTU(axis.min) : undefined,
|
||||||
|
max: axis.maxHard ? NTU(axis.max) : undefined,
|
||||||
|
position: axis.right ? 'right' : 'left',
|
||||||
|
};
|
||||||
|
for (const graph of axis.graphs) {
|
||||||
|
this.buildGraph(graph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGraph(graph: Graph): void {
|
||||||
|
if (!graph.visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const midSuffix = graph.series.type === SeriesType.DELTA || graph.series.type === SeriesType.BOOL ? "" : " Ø"
|
||||||
|
const min: Dataset = graph.min ? this.newDataset(graph, " min", false) : null;
|
||||||
|
const mid: Dataset = graph.mid ? this.newDataset(graph, midSuffix, min ? '-1' : graph.series.type === SeriesType.BOOL) : null;
|
||||||
|
const max: Dataset = graph.max ? this.newDataset(graph, " max", min || mid ? '-1' : false) : null;
|
||||||
|
switch (graph.series.type) {
|
||||||
|
case SeriesType.BOOL:
|
||||||
|
this.graphSubscriptions.push(this.boolService.subscribe(bool => this.updateBool(bool, mid), [graph.series.id]));
|
||||||
|
break;
|
||||||
|
case SeriesType.DELTA:
|
||||||
|
this.graphSubscriptions.push(this.deltaService.subscribe(delta => this.updateDelta(delta, mid), [graph.series.id, graph.axis.plot.interval.name]));
|
||||||
|
break;
|
||||||
|
case SeriesType.VARYING:
|
||||||
|
this.graphSubscriptions.push(this.varyingService.subscribe(varying => this.updateVarying(varying, min, mid, max), [graph.series.id, graph.axis.plot.interval.name]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.points(graph, min, mid, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private newDataset(graph: Graph, suffix: string, fill: string | number | boolean): Dataset {
|
||||||
|
this.chart.data.datasets.push({
|
||||||
|
data: [],
|
||||||
|
label: `${graph.name || graph.series.name}${suffix}${unitInBrackets(graph.series.unit)}`,
|
||||||
|
type: graph.type as "line", // TODO
|
||||||
|
fill: fill || (graph.stack ? 'stack' : false),
|
||||||
|
stack: graph.stack || undefined,
|
||||||
|
borderWidth: 1,
|
||||||
|
// barThickness: 'flex', // TODO
|
||||||
|
borderColor: graph.color,
|
||||||
|
pointRadius: graph.series.type === SeriesType.BOOL ? 0 : 4,
|
||||||
|
pointBackgroundColor: graph.color,
|
||||||
|
backgroundColor: graph.color + "33",
|
||||||
|
yAxisID: "y" + graph.axis.id,
|
||||||
|
pointStyle: graph.type === 'bar' || graph.series.type === SeriesType.BOOL ? 'rect' : 'crossRot',
|
||||||
|
spanGaps: graph.series.type === SeriesType.BOOL ? Infinity : graph.axis.plot.interval.spanGaps,
|
||||||
|
});
|
||||||
|
return this.chart.data.datasets[this.chart.data.datasets.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void {
|
||||||
|
this.serieService.points(
|
||||||
|
graph.series,
|
||||||
|
graph.axis.plot.interval,
|
||||||
|
graph.axis.plot.offset,
|
||||||
|
graph.axis.plot.duration,
|
||||||
|
points => {
|
||||||
|
if (graph.series.type === SeriesType.BOOL) {
|
||||||
|
mid.data = toBool(points, graph.factor);
|
||||||
|
} else if (graph.series.type === SeriesType.DELTA) {
|
||||||
|
mid.data = toDelta(points, graph.factor);
|
||||||
|
} else if (graph.series.type === SeriesType.VARYING) {
|
||||||
|
if (min) {
|
||||||
|
min.data = toMin(points, graph.factor);
|
||||||
|
}
|
||||||
|
if (max) {
|
||||||
|
max.data = toMax(points, graph.factor);
|
||||||
|
}
|
||||||
|
if (mid) {
|
||||||
|
mid.data = toAvg(points, graph.factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.chart.update();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly updatePlot = (fresh: Plot): void => {
|
||||||
|
const index = this.plotList.findIndex(plot => plot.id === fresh.id);
|
||||||
|
if (fresh.deleted) {
|
||||||
|
if (index >= 0) {
|
||||||
|
this.plotList.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < 0) {
|
||||||
|
this.plotList.push(fresh);
|
||||||
|
} else {
|
||||||
|
this.plotList[index] = fresh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.plotList = this.plotList.sort(Plot.comparePosition);
|
||||||
|
|
||||||
|
const selected = this.plot?.id === fresh.id;
|
||||||
|
if (fresh.deleted) {
|
||||||
|
if (selected) {
|
||||||
|
const previousIndex = Math.max(0, index - 1);
|
||||||
|
const previousPlot = this.plotList[previousIndex];
|
||||||
|
this.buildPlot(previousPlot);
|
||||||
|
}
|
||||||
|
} else if (!this.plot || selected) {
|
||||||
|
this.buildPlot(fresh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected readonly updateSeries = (fresh: Series): void => {
|
||||||
|
const index = this.seriesList.findIndex(series => series.id === fresh.id);
|
||||||
|
if (index < 0) {
|
||||||
|
this.seriesList.push(fresh);
|
||||||
|
} else if (fresh.deleted) {
|
||||||
|
this.seriesList.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.seriesList[index] = fresh;
|
||||||
|
}
|
||||||
|
this.seriesList = this.seriesList.sort(Series.compareName);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected groups(): string[] {
|
||||||
|
return Object.keys(Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBool(bool: Bool, mid: Dataset): void {
|
||||||
|
this.updatePoint(mid, bool.date, bool.state ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDelta(delta: Delta, mid: Dataset): void {
|
||||||
|
this.updatePoint(mid, delta.date, delta.last - delta.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVarying(varying: Varying, min: Dataset | null, avg: Dataset | null, max: Dataset | null): void {
|
||||||
|
if (min) {
|
||||||
|
this.updatePoint(min, varying.date, varying.min);
|
||||||
|
}
|
||||||
|
if (avg) {
|
||||||
|
this.updatePoint(avg, varying.date, varying.avg);
|
||||||
|
}
|
||||||
|
if (max) {
|
||||||
|
this.updatePoint(max, varying.date, varying.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePoint(dataset: Dataset, date: Date, y: number): void {
|
||||||
|
const x = date.getTime();
|
||||||
|
const point = dataset.data.filter((p: PointElement) => p.x === x)[0];
|
||||||
|
if (point) {
|
||||||
|
if (point.y !== y) {
|
||||||
|
point.y = y;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataset.data.push({x: x, y: y}); // TODO check if this is a LIVE/SCROLLING plot (right end of plot is 'now')
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateAndBuild = (plot: Plot): void => {
|
||||||
|
this.updatePlot(plot);
|
||||||
|
this.buildPlot(plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<div #container class="container">
|
|
||||||
<canvas #chartCanvas></canvas>
|
|
||||||
</div>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
148
src/main/angular/src/app/plot/plot.service.ts
Normal file
148
src/main/angular/src/app/plot/plot.service.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService, CrudService, Next} from '../COMMON';
|
||||||
|
import {Plot} from './Plot';
|
||||||
|
import {Axis} from './Axis';
|
||||||
|
import {Graph} from './Graph';
|
||||||
|
import {Group} from './Group';
|
||||||
|
import {Interval} from '../series/Interval';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PlotService extends CrudService<Plot> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ["Plot"], Plot.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotCreate(next: Next<Plot>): void {
|
||||||
|
this.getSingle(["create"], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotDuplicate(plot: Plot, next: Next<Plot>): void {
|
||||||
|
this.getSingle([plot.id, "duplicate"], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotDelete(plot: Plot, next: Next<Plot>): void {
|
||||||
|
this.getSingle([plot.id, "delete"], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotAddAxis(plot: Plot, next: Next<Plot>): void {
|
||||||
|
this.getSingle([plot.id, 'addAxis'], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotName(plot: Plot, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'name'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotInterval(plot: Plot, value: Interval, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'interval'], value.name, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotOffset(plot: Plot, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'offset'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotDuration(plot: Plot, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'duration'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotDashboard(plot: Plot, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'dashboard'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
plotPosition(plot: Plot, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle([plot.id, 'position'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisAddGraph(axis: Axis, next: Next<Plot>): void {
|
||||||
|
this.getSingle(['Axis', axis.id, 'addGraph'], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisDelete(axis: Axis, next?: Next<Plot>): void {
|
||||||
|
this.getSingle(['Axis', axis.id, 'delete'], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisVisible(axis: Axis, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'visible'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisName(axis: Axis, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'name'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisUnit(axis: Axis, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'unit'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisRight(axis: Axis, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'right'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisMin(axis: Axis, value: number | null, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'min'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisMinHard(axis: Axis, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'minHard'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisMax(axis: Axis, value: number | null, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'max'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
axisMaxHard(axis: Axis, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Axis', axis.id, 'maxHard'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphDelete(graph: Graph, next?: Next<Plot>): void {
|
||||||
|
this.getSingle(['Graph', graph.id, 'delete'], next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphVisible(graph: Graph, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'visible'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphName(graph: Graph, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'name'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphSeries(graph: Graph, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'series'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphColor(graph: Graph, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'color'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphFactor(graph: Graph, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'factor'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphGroup(graph: Graph, value: Group, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'group'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphStack(graph: Graph, value: string, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'stack'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphMin(graph: Graph, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'min'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphMax(graph: Graph, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'max'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphAvg(graph: Graph, value: boolean, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'avg'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphAxis(graph: Graph, value: number, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'axis'], value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,202 +0,0 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
|
|
||||||
import {BarController, BarElement, CategoryScale, Chart, ChartDataset, ChartType, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
|
||||||
import {SeriesService} from "../series/series.service";
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import {Series} from '../series/Series';
|
|
||||||
import {Interval} from '../series/Interval';
|
|
||||||
import {MinMaxAvg, PointMapper, toAvg, toBool, toDelta, toMax, toMin} from '../series/MinMaxAvg';
|
|
||||||
import {SeriesType} from '../series/SeriesType';
|
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
BarController,
|
|
||||||
BarElement,
|
|
||||||
LineController,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
Filler,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ENERGY_PRODUCE = 1;
|
|
||||||
|
|
||||||
const POWER_PRODUCE = 1 + 1;
|
|
||||||
|
|
||||||
const ENERGY_DELIVER = 5;
|
|
||||||
|
|
||||||
const POWER_DELIVER = 5 + 1;
|
|
||||||
|
|
||||||
const ENERGY_PURCHASE = 3;
|
|
||||||
|
|
||||||
const POWER_PURCHASE = 3 + 1;
|
|
||||||
|
|
||||||
const GARDEN_TEMPERATURE = 8;
|
|
||||||
|
|
||||||
const BEDROOM_TEMPERATURE = 12;
|
|
||||||
|
|
||||||
const BUFFER_TEMPERATURE = 24;
|
|
||||||
|
|
||||||
const CIRCUIT_SUPPLY_TEMPERATURE = 20;
|
|
||||||
|
|
||||||
const HEATER = 28;
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-plot',
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './plot.html',
|
|
||||||
styleUrl: './plot.less'
|
|
||||||
})
|
|
||||||
export class Plot implements AfterViewInit, OnDestroy {
|
|
||||||
|
|
||||||
@ViewChild('chartCanvas')
|
|
||||||
canvasRef!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
@ViewChild('container')
|
|
||||||
chartContainer!: ElementRef<HTMLDivElement>;
|
|
||||||
|
|
||||||
private chart!: Chart;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly seriesService: SeriesService,
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
private interval = Interval.FIVE;
|
|
||||||
|
|
||||||
private FACTOR = 0.5;
|
|
||||||
|
|
||||||
private duration = (this.interval === Interval.FIVE ? 24 * 60 / 5 : this.interval === Interval.HOUR ? 7 * 24 : this.interval === Interval.DAY ? 31 : this.interval === Interval.WEEK ? 52 : 99) * this.FACTOR;
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
datasets: [],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
animation: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
position: 'nearest',
|
|
||||||
mode: 'x',
|
|
||||||
usePointStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scaleRight = false;
|
|
||||||
this.fetch(HEATER, "line", '#ff7373', 1, undefined, true, MinMaxAvg.AVG);
|
|
||||||
this.fetch(ENERGY_PRODUCE, "bar", '#53d32b', 1, 'a', false, MinMaxAvg.AVG);
|
|
||||||
this.fetch(ENERGY_PURCHASE, "bar", '#ff8000', 1, 'a', false, MinMaxAvg.AVG);
|
|
||||||
this.fetch(ENERGY_DELIVER, "bar", '#ff5bfc', -1, 'a', false, MinMaxAvg.AVG);
|
|
||||||
// this.fetch(POWER_PRODUCE, "bar", '#53d32b', 1, undefined, false);
|
|
||||||
// this.fetch(POWER_PURCHASE, "bar", '#ff8000', 1, undefined, false);
|
|
||||||
// this.fetch(POWER_DELIVER, "bar", '#ff5bfc', -1, undefined, false);
|
|
||||||
this.fetch(GARDEN_TEMPERATURE, "line", '#00aa00', 1, undefined, false, MinMaxAvg.AVG);
|
|
||||||
this.fetch(BEDROOM_TEMPERATURE, "line", '#0000FF', 1, undefined, false, MinMaxAvg.AVG);
|
|
||||||
this.fetch(BUFFER_TEMPERATURE, "line", '#AA00AA', 1, undefined, false, MinMaxAvg.AVG);
|
|
||||||
this.fetch(CIRCUIT_SUPPLY_TEMPERATURE, "line", '#FF0000', 1, undefined, false, MinMaxAvg.AVG);
|
|
||||||
// this.fetch(POWER_PRODUCE, "line", '#ffc400', 1, undefined, false, MinMaxAvg.MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
scaleRight: boolean = false;
|
|
||||||
|
|
||||||
private fetch(id: number, type: ChartType, color: string, factor: number, stack: string | undefined, fill: any, minMaxAvg: MinMaxAvg) {
|
|
||||||
this.chart.data.datasets.push({
|
|
||||||
data: [],
|
|
||||||
type: type,
|
|
||||||
fill: fill,
|
|
||||||
stack: stack,
|
|
||||||
pointRadius: 4,
|
|
||||||
borderWidth: 1,
|
|
||||||
barThickness: 'flex',
|
|
||||||
borderColor: color,
|
|
||||||
pointBackgroundColor: color,
|
|
||||||
backgroundColor: color + "33",
|
|
||||||
});
|
|
||||||
const dataset: ChartDataset<any, any> = this.chart.data.datasets[this.chart.data.datasets.length - 1];
|
|
||||||
this.seriesService.getById(id, series => {
|
|
||||||
if (this.chart.options.scales) {
|
|
||||||
if (!this.chart.options.scales["y-" + series.unit]) {
|
|
||||||
this.chart.options.scales["y-" + series.unit] = {
|
|
||||||
display: series.unit !== "",
|
|
||||||
min: series.unit !== "" ? undefined : 0,
|
|
||||||
max: series.unit !== "" ? undefined : 4,
|
|
||||||
beginAtZero: true,
|
|
||||||
position: this.scaleRight ? 'right' : 'left',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: series.unit,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.scaleRight = !this.scaleRight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataset.label = series.name;
|
|
||||||
dataset.yAxisID = "y-" + series.unit;
|
|
||||||
dataset.pointStyle = type === 'bar' || series.type === SeriesType.BOOL ? 'rect' : 'crossRot';
|
|
||||||
dataset.spanGaps = series.type === SeriesType.BOOL ? Infinity : this.interval.spanGaps;
|
|
||||||
if (series.type === SeriesType.BOOL) {
|
|
||||||
dataset.pointRadius = 0;
|
|
||||||
}
|
|
||||||
this.points(series, this.interval, 0, this.duration, minMaxAvg, factor, dataset);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.chart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private points(
|
|
||||||
series: Series,
|
|
||||||
interval: Interval,
|
|
||||||
offset: number,
|
|
||||||
duration: number,
|
|
||||||
type: MinMaxAvg,
|
|
||||||
factor: number,
|
|
||||||
dataset: ChartDataset<any, any>[][number],
|
|
||||||
) {
|
|
||||||
let mapper: PointMapper;
|
|
||||||
switch (series.type) {
|
|
||||||
case SeriesType.BOOL:
|
|
||||||
mapper = toBool;
|
|
||||||
break;
|
|
||||||
case SeriesType.DELTA:
|
|
||||||
mapper = toDelta;
|
|
||||||
break;
|
|
||||||
case SeriesType.VARYING:
|
|
||||||
mapper = type === MinMaxAvg.MIN ? toMin : type === MinMaxAvg.MAX ? toMax : toAvg;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.seriesService.points(
|
|
||||||
series,
|
|
||||||
interval,
|
|
||||||
offset,
|
|
||||||
duration,
|
|
||||||
points => {
|
|
||||||
dataset.data = mapper(points, factor);
|
|
||||||
this.chart.update();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,35 +2,40 @@ import {validateString} from "../COMMON";
|
|||||||
|
|
||||||
export class Interval {
|
export class Interval {
|
||||||
|
|
||||||
private static values: Interval[] = [];
|
protected static _values: Interval[] = [];
|
||||||
|
|
||||||
static readonly FIVE = new Interval("FIVE", 5 * 60 * 1000);
|
static readonly FIVE = new Interval("FIVE", "5 Minuten", 5 * 60 * 1000);
|
||||||
|
|
||||||
static readonly HOUR = new Interval("HOUR", 60 * 60 * 1000);
|
static readonly HOUR = new Interval("HOUR", "Stunden", 60 * 60 * 1000);
|
||||||
|
|
||||||
static readonly DAY = new Interval("DAY", 24 * 60 * 60 * 1000);
|
static readonly DAY = new Interval("DAY", "Tage", 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
static readonly WEEK = new Interval("WEEK", 7 * 24 * 60 * 60 * 1000);
|
static readonly WEEK = new Interval("WEEK", "Wochen", 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
static readonly MONTH = new Interval("MONTH", 31 * 24 * 60 * 60 * 1000);
|
static readonly MONTH = new Interval("MONTH", "Monate", 31 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
static readonly YEAR = new Interval("YEAR", 366 * 24 * 60 * 60 * 1000);
|
static readonly YEAR = new Interval("YEAR", "Jahre", 366 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
|
readonly display: string,
|
||||||
readonly spanGaps: number,
|
readonly spanGaps: number,
|
||||||
) {
|
) {
|
||||||
Interval.values.push(this);
|
Interval._values.push(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: any): Interval {
|
static fromJson(json: any): Interval {
|
||||||
const name = validateString(json)
|
const name = validateString(json)
|
||||||
const interval = Interval.values.filter(i => i.name === name)[0];
|
const interval = Interval._values.filter(i => i.name === name)[0];
|
||||||
if (!interval) {
|
if (!interval) {
|
||||||
throw new Error(`Not an Interval: ${JSON.stringify(json)}`);
|
throw new Error(`Not an Interval: ${JSON.stringify(json)}`);
|
||||||
}
|
}
|
||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get values(): Interval[] {
|
||||||
|
return Interval._values;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import {validateNumber, validateString} from "../COMMON";
|
import {mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON";
|
||||||
import {SeriesType} from './SeriesType';
|
import {SeriesType} from './SeriesType';
|
||||||
|
import {formatNumber} from '@angular/common';
|
||||||
|
|
||||||
export class Series {
|
export class Series {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: number,
|
readonly id: number,
|
||||||
|
readonly version: number,
|
||||||
|
readonly deleted: boolean,
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly unit: string,
|
readonly unit: string,
|
||||||
readonly type: SeriesType,
|
readonly type: SeriesType,
|
||||||
|
readonly decimals: number,
|
||||||
|
readonly value: number | null,
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -15,10 +20,30 @@ export class Series {
|
|||||||
static fromJson(json: any): Series {
|
static fromJson(json: any): Series {
|
||||||
return new Series(
|
return new Series(
|
||||||
validateNumber(json.id),
|
validateNumber(json.id),
|
||||||
|
validateNumber(json.version),
|
||||||
|
validateBoolean(json.deleted),
|
||||||
validateString(json.name),
|
validateString(json.name),
|
||||||
validateString(json.unit),
|
validateString(json.unit),
|
||||||
validateString(json.type) as SeriesType,
|
validateString(json.type) as SeriesType,
|
||||||
|
validateNumber(json.decimals),
|
||||||
|
mapNotNull(json.value, validateNumber),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get digitString(): string {
|
||||||
|
return `0.${this.decimals}-${this.decimals}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueString(): string {
|
||||||
|
const result = (this.value === null ? "-" : this.type === SeriesType.BOOL ? this.value > 0 ? "EIN" : "AUS" : formatNumber(this.value, "de-DE", this.digitString)) + "";
|
||||||
|
if (this.unit) {
|
||||||
|
return result + " " + this.unit;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compareName(a: Series, b: Series) {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/main/angular/src/app/series/bool/bool-service.ts
Normal file
40
src/main/angular/src/app/series/bool/bool-service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService, CrudService, validateBoolean, validateDate} from '../../COMMON';
|
||||||
|
import {Series} from '../Series';
|
||||||
|
|
||||||
|
export class Bool {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly series: Series,
|
||||||
|
readonly date: Date,
|
||||||
|
readonly end: Date,
|
||||||
|
readonly state: boolean,
|
||||||
|
readonly terminated: boolean,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Bool {
|
||||||
|
return new Bool(
|
||||||
|
Series.fromJson(json.series),
|
||||||
|
validateDate(json.date),
|
||||||
|
validateDate(json.end),
|
||||||
|
validateBoolean(json.state),
|
||||||
|
validateBoolean(json.terminated),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BoolService extends CrudService<Bool> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ["Bool"], Bool.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
src/main/angular/src/app/series/delta/delta-service.ts
Normal file
38
src/main/angular/src/app/series/delta/delta-service.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService, CrudService, validateDate, validateNumber} from '../../COMMON';
|
||||||
|
import {Series} from '../Series';
|
||||||
|
|
||||||
|
export class Delta {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly series: Series,
|
||||||
|
readonly date: Date,
|
||||||
|
readonly first: number,
|
||||||
|
readonly last: number,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Delta {
|
||||||
|
return new Delta(
|
||||||
|
Series.fromJson(json.series),
|
||||||
|
validateDate(json.date),
|
||||||
|
validateNumber(json.first),
|
||||||
|
validateNumber(json.last),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DeltaService extends CrudService<Delta> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ["Delta"], Delta.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
src/main/angular/src/app/series/varying/varying-service.ts
Normal file
40
src/main/angular/src/app/series/varying/varying-service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService, CrudService, validateDate, validateNumber} from '../../COMMON';
|
||||||
|
import {Series} from '../Series';
|
||||||
|
|
||||||
|
export class Varying {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly series: Series,
|
||||||
|
readonly date: Date,
|
||||||
|
readonly min: number,
|
||||||
|
readonly max: number,
|
||||||
|
readonly avg: number,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Varying {
|
||||||
|
return new Varying(
|
||||||
|
Series.fromJson(json.series),
|
||||||
|
validateDate(json.date),
|
||||||
|
validateNumber(json.min),
|
||||||
|
validateNumber(json.max),
|
||||||
|
validateNumber(json.avg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class VaryingService extends CrudService<Varying> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: ApiService,
|
||||||
|
) {
|
||||||
|
super(api, ["Varying"], Varying.fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,3 +2,28 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
padding: 0.25em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 0.125em;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
package de.ph87.data;
|
package de.ph87.data;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.Plot;
|
||||||
|
import de.ph87.data.plot.PlotRepository;
|
||||||
|
import de.ph87.data.plot.axis.Axis;
|
||||||
|
import de.ph87.data.plot.axis.AxisRepository;
|
||||||
|
import de.ph87.data.plot.axis.graph.Graph;
|
||||||
|
import de.ph87.data.plot.axis.graph.GraphRepository;
|
||||||
import de.ph87.data.series.Series;
|
import de.ph87.data.series.Series;
|
||||||
import de.ph87.data.series.SeriesRepository;
|
import de.ph87.data.series.SeriesRepository;
|
||||||
import de.ph87.data.series.SeriesType;
|
import de.ph87.data.series.SeriesType;
|
||||||
@ -26,9 +32,27 @@ public class DemoService {
|
|||||||
|
|
||||||
private final TopicRepository topicRepository;
|
private final TopicRepository topicRepository;
|
||||||
|
|
||||||
|
private final PlotRepository plotRepository;
|
||||||
|
|
||||||
|
private final AxisRepository axisRepository;
|
||||||
|
|
||||||
|
private final GraphRepository graphRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void init() {
|
public void init() {
|
||||||
|
topics();
|
||||||
|
// plots();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void topics() {
|
||||||
|
final Series fallbackRelay0 = series("fallback/relay0", "", SeriesType.BOOL, 5);
|
||||||
|
topic(
|
||||||
|
"fallback/relay0",
|
||||||
|
"$.timestamp",
|
||||||
|
new TopicQuery(fallbackRelay0, "$.state", "$.stateEpoch", "true")
|
||||||
|
);
|
||||||
|
|
||||||
final Series infraredHeater = series("infraredHeater/state", "", SeriesType.BOOL, 5);
|
final Series infraredHeater = series("infraredHeater/state", "", SeriesType.BOOL, 5);
|
||||||
topic(
|
topic(
|
||||||
"Infrarotheizung",
|
"Infrarotheizung",
|
||||||
@ -108,6 +132,69 @@ public class DemoService {
|
|||||||
topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value"));
|
topic("cistern/volume/PatrixJson", "$.date", new TopicQuery(cisternVolume, "$.value"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void plots() {
|
||||||
|
plotRepository.deleteAll();
|
||||||
|
|
||||||
|
final Plot plot = plotRepository.save(new Plot());
|
||||||
|
plot.setName("Test");
|
||||||
|
|
||||||
|
final Axis energy = axisRepository.save(new Axis(plot));
|
||||||
|
plot.addAxis(energy);
|
||||||
|
energy.setRight(true);
|
||||||
|
energy.setName("Energie");
|
||||||
|
energy.setUnit("kWh");
|
||||||
|
|
||||||
|
final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow();
|
||||||
|
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
|
||||||
|
electricityEnergyPurchaseGraph.setName("Bezug");
|
||||||
|
electricityEnergyPurchaseGraph.setColor("#FF8800");
|
||||||
|
energy.addGraph(electricityEnergyPurchaseGraph);
|
||||||
|
|
||||||
|
final Axis temperature = axisRepository.save(new Axis(plot));
|
||||||
|
plot.addAxis(temperature);
|
||||||
|
temperature.setRight(true);
|
||||||
|
temperature.setName("Temperatur");
|
||||||
|
temperature.setUnit("°C");
|
||||||
|
|
||||||
|
final Series bedroomTemperature = seriesRepository.findByName("bedroom/temperature").orElseThrow();
|
||||||
|
final Graph bedroomTemperatureGraph = graphRepository.save(new Graph(temperature, bedroomTemperature));
|
||||||
|
bedroomTemperatureGraph.setName("Schlafzimmer");
|
||||||
|
bedroomTemperatureGraph.setColor("#0000FF");
|
||||||
|
bedroomTemperatureGraph.setMin(true);
|
||||||
|
bedroomTemperatureGraph.setAvg(false);
|
||||||
|
bedroomTemperatureGraph.setMax(true);
|
||||||
|
temperature.addGraph(bedroomTemperatureGraph);
|
||||||
|
|
||||||
|
final Axis status = axisRepository.save(new Axis(plot));
|
||||||
|
plot.addAxis(status);
|
||||||
|
status.setVisible(false);
|
||||||
|
status.setRight(true);
|
||||||
|
status.setName("Status");
|
||||||
|
|
||||||
|
final Series fallbackRelay0 = seriesRepository.findByName("fallback/relay0").orElseThrow();
|
||||||
|
final Graph fallbackRelay0Graph = graphRepository.save(new Graph(status, fallbackRelay0));
|
||||||
|
fallbackRelay0Graph.setName("FallbackRelay0");
|
||||||
|
fallbackRelay0Graph.setColor("#00FF00");
|
||||||
|
status.addGraph(fallbackRelay0Graph);
|
||||||
|
|
||||||
|
final Series infrared = seriesRepository.findByName("infraredHeater/state").orElseThrow();
|
||||||
|
final Graph infraredGraph = graphRepository.save(new Graph(status, infrared));
|
||||||
|
infraredGraph.setName("Infrarotheizung");
|
||||||
|
infraredGraph.setColor("#FF00FF");
|
||||||
|
status.addGraph(infraredGraph);
|
||||||
|
|
||||||
|
plotRepository.save(plot);
|
||||||
|
|
||||||
|
axisRepository.save(energy);
|
||||||
|
axisRepository.save(temperature);
|
||||||
|
axisRepository.save(status);
|
||||||
|
|
||||||
|
graphRepository.save(electricityEnergyPurchaseGraph);
|
||||||
|
graphRepository.save(bedroomTemperatureGraph);
|
||||||
|
graphRepository.save(fallbackRelay0Graph);
|
||||||
|
graphRepository.save(infraredGraph);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int expectedEverySeconds) {
|
private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int expectedEverySeconds) {
|
||||||
return seriesRepository
|
return seriesRepository
|
||||||
|
|||||||
@ -15,4 +15,12 @@ public class Helpers {
|
|||||||
return mapper.apply(t);
|
return mapper.apply(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static <T> T or(@Nullable final T t, @NonNull final T r) {
|
||||||
|
if (t == null) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,8 @@ package de.ph87.data.log;
|
|||||||
|
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import jakarta.persistence.ElementCollection;
|
import jakarta.persistence.ElementCollection;
|
||||||
import jakarta.persistence.MappedSuperclass;
|
|
||||||
import jakarta.persistence.OrderColumn;
|
import jakarta.persistence.OrderColumn;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -16,9 +14,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
|
||||||
@MappedSuperclass
|
|
||||||
@NoArgsConstructor
|
|
||||||
public abstract class AbstractEntityLog {
|
public abstract class AbstractEntityLog {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
86
src/main/java/de/ph87/data/plot/Plot.java
Normal file
86
src/main/java/de/ph87/data/plot/Plot.java
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package de.ph87.data.plot;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.axis.Axis;
|
||||||
|
import de.ph87.data.series.data.Interval;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Plot {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, name = "`interval`")
|
||||||
|
private Interval interval = Interval.FIVE;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false, name = "`offset`")
|
||||||
|
private long offset = 0;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private long duration = 288;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean dashboard = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private long position;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@OneToMany(mappedBy = "plot", orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
|
private List<Axis> axes = new ArrayList<>();
|
||||||
|
|
||||||
|
public Plot(final long position) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plot(@NonNull final Plot plot, final long position) {
|
||||||
|
this.name = plot.getName();
|
||||||
|
this.interval = plot.getInterval();
|
||||||
|
this.offset = plot.getOffset();
|
||||||
|
this.duration = plot.getDuration();
|
||||||
|
this.dashboard = plot.isDashboard();
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAxis(@NonNull final Axis axis) {
|
||||||
|
axes.add(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/main/java/de/ph87/data/plot/PlotController.java
Normal file
83
src/main/java/de/ph87/data/plot/PlotController.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package de.ph87.data.plot;
|
||||||
|
|
||||||
|
import de.ph87.data.series.data.Interval;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.ph87.data.Helpers.or;
|
||||||
|
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Plot")
|
||||||
|
public class PlotController {
|
||||||
|
|
||||||
|
private final PlotRepository plotRepository;
|
||||||
|
|
||||||
|
private final PlotService plotService;
|
||||||
|
|
||||||
|
@GetMapping("findAll")
|
||||||
|
public List<PlotDto> findAllDto() {
|
||||||
|
return plotRepository.findAllDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("create")
|
||||||
|
public PlotDto create() {
|
||||||
|
return plotService.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}/duplicate")
|
||||||
|
public PlotDto duplicate(@PathVariable final long id) {
|
||||||
|
return plotService.duplicate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}/delete")
|
||||||
|
public PlotDto delete(@PathVariable final long id) {
|
||||||
|
return plotService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}/addAxis")
|
||||||
|
public PlotDto addAxis(@PathVariable final long id) {
|
||||||
|
return plotService.addAxis(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/name")
|
||||||
|
public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return plotService.set(id, plot -> plot.setName(or(value, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/interval")
|
||||||
|
public PlotDto interval(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return plotService.set(id, plot -> plot.setInterval(Interval.valueOf(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/offset")
|
||||||
|
public PlotDto offset(@PathVariable final long id, @RequestBody final long value) {
|
||||||
|
return plotService.set(id, plot -> plot.setOffset(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/duration")
|
||||||
|
public PlotDto duration(@PathVariable final long id, @RequestBody final long value) {
|
||||||
|
return plotService.set(id, plot -> plot.setDuration(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/dashboard")
|
||||||
|
public PlotDto dashboard(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return plotService.set(id, plot -> plot.setDashboard(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/position")
|
||||||
|
public PlotDto position(@PathVariable final long id, @RequestBody final long value) {
|
||||||
|
return plotService.setPosition(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
src/main/java/de/ph87/data/plot/PlotDto.java
Normal file
50
src/main/java/de/ph87/data/plot/PlotDto.java
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package de.ph87.data.plot;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.axis.AxisDto;
|
||||||
|
import de.ph87.data.series.data.Interval;
|
||||||
|
import de.ph87.data.websocket.IWebsocketMessage;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PlotDto implements IWebsocketMessage {
|
||||||
|
|
||||||
|
public final long id;
|
||||||
|
|
||||||
|
public final long version;
|
||||||
|
|
||||||
|
private final boolean deleted;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Interval interval;
|
||||||
|
|
||||||
|
public final long offset;
|
||||||
|
|
||||||
|
public final long duration;
|
||||||
|
|
||||||
|
public final boolean dashboard;
|
||||||
|
|
||||||
|
public final long position;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<AxisDto> axes;
|
||||||
|
|
||||||
|
public PlotDto(@NonNull final Plot plot, final boolean deleted) {
|
||||||
|
this.id = plot.getId();
|
||||||
|
this.version = plot.getVersion();
|
||||||
|
this.deleted = deleted;
|
||||||
|
this.name = plot.getName();
|
||||||
|
this.interval = plot.getInterval();
|
||||||
|
this.offset = plot.getOffset();
|
||||||
|
this.duration = plot.getDuration();
|
||||||
|
this.dashboard = plot.isDashboard();
|
||||||
|
this.position = plot.getPosition();
|
||||||
|
this.axes = plot.getAxes().stream().map(AxisDto::new).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
src/main/java/de/ph87/data/plot/PlotRepository.java
Normal file
23
src/main/java/de/ph87/data/plot/PlotRepository.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package de.ph87.data.plot;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PlotRepository extends ListCrudRepository<Plot, Long> {
|
||||||
|
|
||||||
|
@Query("select new de.ph87.data.plot.PlotDto(p, false) from Plot p")
|
||||||
|
List<PlotDto> findAllDto();
|
||||||
|
|
||||||
|
@Query("select max(p.position) from Plot p")
|
||||||
|
Optional<Integer> findMaxPosition();
|
||||||
|
|
||||||
|
List<Plot> findAllByPositionGreaterThanAndPositionLessThanEqual(long first, long last);
|
||||||
|
|
||||||
|
List<Plot> findAllByPositionGreaterThanEqualAndPositionLessThan(long first, long last);
|
||||||
|
|
||||||
|
List<Plot> findAllByPositionGreaterThan(long position);
|
||||||
|
|
||||||
|
}
|
||||||
126
src/main/java/de/ph87/data/plot/PlotService.java
Normal file
126
src/main/java/de/ph87/data/plot/PlotService.java
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package de.ph87.data.plot;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.axis.Axis;
|
||||||
|
import de.ph87.data.plot.axis.AxisRepository;
|
||||||
|
import de.ph87.data.plot.axis.graph.Graph;
|
||||||
|
import de.ph87.data.plot.axis.graph.GraphRepository;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PlotService {
|
||||||
|
|
||||||
|
private final PlotRepository plotRepository;
|
||||||
|
|
||||||
|
private final AxisRepository axisRepository;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
private final GraphRepository graphRepository;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto create() {
|
||||||
|
return publish(plotRepository.save(new Plot(getFreePosition())), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto duplicate(final long id) {
|
||||||
|
final Plot plotOriginal = getById(id);
|
||||||
|
final Plot plotCopy = plotRepository.save(new Plot(plotOriginal, getFreePosition()));
|
||||||
|
plotOriginal.getAxes().stream().map(axisOriginal -> duplicateAxis(plotCopy, axisOriginal)).forEach(plotCopy::addAxis);
|
||||||
|
return publish(plotCopy, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Axis duplicateAxis(@NonNull final Plot plotCopy, @NonNull final Axis axisOriginal) {
|
||||||
|
final Axis axisCopy = axisRepository.save(new Axis(plotCopy, axisOriginal));
|
||||||
|
axisOriginal.getGraphs().stream().map(graphOriginal -> duplicateGraph(graphOriginal, axisCopy)).forEach(axisCopy::addGraph);
|
||||||
|
return axisCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Graph duplicateGraph(@NonNull final Graph graphOriginal, @NonNull final Axis axisCopy) {
|
||||||
|
return graphRepository.save(new Graph(axisCopy, graphOriginal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getFreePosition() {
|
||||||
|
return plotRepository.findMaxPosition().orElse(-1) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PlotDto delete(final long id) {
|
||||||
|
final Plot plot = getById(id);
|
||||||
|
plotRepository.delete(plot);
|
||||||
|
plotRepository.findAllByPositionGreaterThan(plot.getPosition()).forEach(this::decrementPosition);
|
||||||
|
return publish(plot, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto addAxis(final long id) {
|
||||||
|
return set(id, plot -> plot.addAxis(axisRepository.save(new Axis(plot))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto set(final long id, @NonNull final Consumer<Plot> modifier) {
|
||||||
|
final Plot plot = getById(id);
|
||||||
|
modifier.accept(plot);
|
||||||
|
return publish(plot, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public PlotDto publish(@NonNull final Plot plot, final boolean deleted) {
|
||||||
|
if (deleted) {
|
||||||
|
log.info("Deleted: plot={}", plot);
|
||||||
|
} else {
|
||||||
|
log.info("Updated: plot={}", plot);
|
||||||
|
}
|
||||||
|
final PlotDto dto = new PlotDto(plot, deleted);
|
||||||
|
applicationEventPublisher.publishEvent(dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Plot getById(final long id) {
|
||||||
|
return plotRepository.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto setPosition(final long id, long newPosition) {
|
||||||
|
final long newPositionBound = Math.max(0, Math.min(newPosition, plotRepository.count() - 1));
|
||||||
|
return set(id, plot -> {
|
||||||
|
if (newPositionBound > plot.getPosition()) {
|
||||||
|
plotRepository.findAllByPositionGreaterThanAndPositionLessThanEqual(plot.getPosition(), newPositionBound).forEach(this::decrementPosition);
|
||||||
|
} else if (newPositionBound < plot.getPosition()) {
|
||||||
|
plotRepository.findAllByPositionGreaterThanEqualAndPositionLessThan(newPositionBound, plot.getPosition()).forEach(this::incrementPosition);
|
||||||
|
}
|
||||||
|
plot.setPosition(newPositionBound);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementPosition(@NonNull final Plot plot) {
|
||||||
|
setPosition(plot, +1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decrementPosition(@NonNull final Plot plot) {
|
||||||
|
setPosition(plot, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPosition(@NonNull final Plot plot, final int delta) {
|
||||||
|
plot.setPosition(plot.getPosition() + delta);
|
||||||
|
publish(plot, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
104
src/main/java/de/ph87/data/plot/axis/Axis.java
Normal file
104
src/main/java/de/ph87/data/plot/axis/Axis.java
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package de.ph87.data.plot.axis;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.Plot;
|
||||||
|
import de.ph87.data.plot.axis.graph.Graph;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Axis {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Plot plot;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String unit = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean visible = true;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false, name = "`right`")
|
||||||
|
private boolean right = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Double min = null;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean minHard = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Double max = null;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean maxHard = false;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@OneToMany(mappedBy = "axis", orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
|
private List<Graph> graphs = new ArrayList<>();
|
||||||
|
|
||||||
|
public Axis(@NonNull final Plot plot) {
|
||||||
|
this.plot = plot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Axis(@NonNull final Plot plot, @NonNull final Axis axis) {
|
||||||
|
this.plot = plot;
|
||||||
|
this.name = axis.name;
|
||||||
|
this.unit = axis.unit;
|
||||||
|
this.visible = axis.visible;
|
||||||
|
this.right = axis.right;
|
||||||
|
this.min = axis.min;
|
||||||
|
this.max = axis.max;
|
||||||
|
this.maxHard = axis.maxHard;
|
||||||
|
this.minHard = axis.minHard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGraph(@NonNull final Graph graph) {
|
||||||
|
graphs.add(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
87
src/main/java/de/ph87/data/plot/axis/AxisController.java
Normal file
87
src/main/java/de/ph87/data/plot/axis/AxisController.java
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package de.ph87.data.plot.axis;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.PlotDto;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.ph87.data.Helpers.or;
|
||||||
|
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Plot/Axis")
|
||||||
|
public class AxisController {
|
||||||
|
|
||||||
|
private final AxisService axisService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("{id}/delete")
|
||||||
|
public PlotDto delete(@PathVariable final long id) {
|
||||||
|
return axisService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}/addGraph")
|
||||||
|
public PlotDto addGraph(@PathVariable final long id) {
|
||||||
|
return axisService.addGraph(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/visible")
|
||||||
|
public PlotDto visible(@PathVariable long id, @RequestBody final boolean value) {
|
||||||
|
return axisService.set(id, axis -> axis.setVisible(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/name")
|
||||||
|
public PlotDto name(@PathVariable long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return axisService.set(id, axis -> axis.setName(or(value, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/unit")
|
||||||
|
public PlotDto unit(@PathVariable long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return axisService.set(id, axis -> axis.setUnit(or(value, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/right")
|
||||||
|
public PlotDto right(@PathVariable long id, @RequestBody final boolean value) {
|
||||||
|
return axisService.set(id, axis -> axis.setRight(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/min")
|
||||||
|
public PlotDto min(@PathVariable long id, @RequestBody(required = false) @Nullable final Double value) {
|
||||||
|
return axisService.set(id, axis -> axis.setMin(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/minHard")
|
||||||
|
public PlotDto minHard(@PathVariable long id, @RequestBody final boolean value) {
|
||||||
|
return axisService.set(id, axis -> axis.setMinHard(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/max")
|
||||||
|
public PlotDto max(@PathVariable long id, @RequestBody(required = false) @Nullable final Double value) {
|
||||||
|
return axisService.set(id, axis -> axis.setMax(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/maxHard")
|
||||||
|
public PlotDto maxHard(@PathVariable long id, @RequestBody final boolean value) {
|
||||||
|
return axisService.set(id, axis -> axis.setMaxHard(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/position")
|
||||||
|
public PlotDto position(@PathVariable final long id, @RequestBody final int value) {
|
||||||
|
return axisService.set(id, axis -> {
|
||||||
|
final List<Axis> list = axis.getPlot().getAxes();
|
||||||
|
list.remove(axis);
|
||||||
|
list.add(Math.max(0, Math.min(value, list.size())), axis);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
src/main/java/de/ph87/data/plot/axis/AxisDto.java
Normal file
54
src/main/java/de/ph87/data/plot/axis/AxisDto.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package de.ph87.data.plot.axis;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.axis.graph.GraphDto;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AxisDto {
|
||||||
|
|
||||||
|
public final long id;
|
||||||
|
|
||||||
|
public final long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String unit;
|
||||||
|
|
||||||
|
public final boolean visible;
|
||||||
|
|
||||||
|
public final boolean right;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final Double min;
|
||||||
|
|
||||||
|
public final boolean minHard;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final Double max;
|
||||||
|
|
||||||
|
public final boolean maxHard;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<GraphDto> graphs;
|
||||||
|
|
||||||
|
public AxisDto(@NonNull final Axis axis) {
|
||||||
|
this.id = axis.getId();
|
||||||
|
this.version = axis.getVersion();
|
||||||
|
this.name = axis.getName();
|
||||||
|
this.unit = axis.getUnit();
|
||||||
|
this.visible = axis.isVisible();
|
||||||
|
this.right = axis.isRight();
|
||||||
|
this.min = axis.getMin();
|
||||||
|
this.minHard = axis.isMinHard();
|
||||||
|
this.max = axis.getMax();
|
||||||
|
this.maxHard = axis.isMaxHard();
|
||||||
|
this.graphs = axis.getGraphs().stream().map(GraphDto::new).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/main/java/de/ph87/data/plot/axis/AxisRepository.java
Normal file
7
src/main/java/de/ph87/data/plot/axis/AxisRepository.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package de.ph87.data.plot.axis;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
public interface AxisRepository extends ListCrudRepository<Axis, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
60
src/main/java/de/ph87/data/plot/axis/AxisService.java
Normal file
60
src/main/java/de/ph87/data/plot/axis/AxisService.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package de.ph87.data.plot.axis;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.PlotDto;
|
||||||
|
import de.ph87.data.plot.PlotService;
|
||||||
|
import de.ph87.data.plot.axis.graph.Graph;
|
||||||
|
import de.ph87.data.plot.axis.graph.GraphRepository;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import de.ph87.data.series.SeriesRepository;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AxisService {
|
||||||
|
|
||||||
|
private final AxisRepository axisRepository;
|
||||||
|
|
||||||
|
private final GraphRepository graphRepository;
|
||||||
|
|
||||||
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
|
private final PlotService plotService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto delete(final long id) {
|
||||||
|
final Axis axis = getById(id);
|
||||||
|
axisRepository.delete(axis);
|
||||||
|
axis.getPlot().getAxes().remove(axis);
|
||||||
|
log.info("Deleted: axis={}", axis);
|
||||||
|
return plotService.publish(axis.getPlot(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto addGraph(final long axisId) {
|
||||||
|
final Series series = seriesRepository.findFirstByOrderByNameAsc().orElseThrow();
|
||||||
|
return set(axisId, axis -> axis.addGraph(graphRepository.save(new Graph(axis, series))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto set(final long id, @NonNull final Consumer<Axis> modifier) {
|
||||||
|
final Axis axis = getById(id);
|
||||||
|
modifier.accept(axis);
|
||||||
|
return plotService.publish(axis.getPlot(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Axis getById(final long id) {
|
||||||
|
return axisRepository.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
104
src/main/java/de/ph87/data/plot/axis/graph/Graph.java
Normal file
104
src/main/java/de/ph87/data/plot/axis/graph/Graph.java
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.axis.Axis;
|
||||||
|
import de.ph87.data.series.Series;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Graph {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Axis axis;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private Series series;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean visible = true;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String color = "#FF0000";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private double factor = 1;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, name = "`group`")
|
||||||
|
private Group group = Group.NONE;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String stack = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean min = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean max = false;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean avg = true;
|
||||||
|
|
||||||
|
public Graph(@NonNull final Axis axis, @NonNull final Series series) {
|
||||||
|
this.axis = axis;
|
||||||
|
this.series = series;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Graph(@NonNull final Axis axis, @NonNull final Graph graph) {
|
||||||
|
this.axis = axis;
|
||||||
|
this.series = graph.series;
|
||||||
|
this.name = graph.name;
|
||||||
|
this.visible = graph.visible;
|
||||||
|
this.color = graph.color;
|
||||||
|
this.factor = graph.factor;
|
||||||
|
this.group = graph.group;
|
||||||
|
this.stack = graph.stack;
|
||||||
|
this.min = graph.min;
|
||||||
|
this.max = graph.max;
|
||||||
|
this.avg = graph.avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
src/main/java/de/ph87/data/plot/axis/graph/GraphController.java
Normal file
103
src/main/java/de/ph87/data/plot/axis/graph/GraphController.java
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.PlotDto;
|
||||||
|
import de.ph87.data.plot.axis.AxisRepository;
|
||||||
|
import de.ph87.data.series.SeriesRepository;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.ph87.data.Helpers.or;
|
||||||
|
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Plot/Graph")
|
||||||
|
public class GraphController {
|
||||||
|
|
||||||
|
private final GraphService graphService;
|
||||||
|
|
||||||
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
|
private final AxisRepository axisRepository;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("{id}/delete")
|
||||||
|
public PlotDto delete(@PathVariable final long id) {
|
||||||
|
return graphService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/visible")
|
||||||
|
public PlotDto visible(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return graphService.set(id, graph -> graph.setVisible(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/name")
|
||||||
|
public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return graphService.set(id, graph -> graph.setName(or(value, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/series")
|
||||||
|
public PlotDto series(@PathVariable final long id, @RequestBody final long value) {
|
||||||
|
return graphService.set(id, graph -> graph.setSeries(seriesRepository.findById(value).orElseThrow()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/color")
|
||||||
|
public PlotDto color(@PathVariable final long id, @RequestBody @NonNull final String value) {
|
||||||
|
return graphService.set(id, graph -> graph.setColor(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/factor")
|
||||||
|
public PlotDto factor(@PathVariable final long id, @RequestBody final double value) {
|
||||||
|
return graphService.set(id, graph -> graph.setFactor(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/group")
|
||||||
|
public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String value) {
|
||||||
|
return graphService.set(id, graph -> graph.setGroup(Group.valueOf(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/stack")
|
||||||
|
public PlotDto stack(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
|
return graphService.set(id, graph -> graph.setStack(or(value, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/min")
|
||||||
|
public PlotDto min(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return graphService.set(id, graph -> graph.setMin(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/max")
|
||||||
|
public PlotDto max(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return graphService.set(id, graph -> graph.setMax(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/avg")
|
||||||
|
public PlotDto avg(@PathVariable final long id, @RequestBody final boolean value) {
|
||||||
|
return graphService.set(id, graph -> graph.setAvg(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/axis")
|
||||||
|
public PlotDto axis(@PathVariable final long id, @RequestBody final long value) {
|
||||||
|
return graphService.set(id, graph -> graph.setAxis(axisRepository.findById(value).orElseThrow()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/position")
|
||||||
|
public PlotDto position(@PathVariable final long id, @RequestBody final int value) {
|
||||||
|
return graphService.set(id, graph -> {
|
||||||
|
final List<Graph> list = graph.getAxis().getGraphs();
|
||||||
|
list.remove(graph);
|
||||||
|
list.add(Math.max(0, Math.min(value, list.size())), graph);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java
Normal file
54
src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.series.SeriesDto;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GraphDto {
|
||||||
|
|
||||||
|
public final long id;
|
||||||
|
|
||||||
|
public final long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final SeriesDto series;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
public final boolean visible;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String color;
|
||||||
|
|
||||||
|
public final double factor;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Group group;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String stack;
|
||||||
|
|
||||||
|
public final boolean min;
|
||||||
|
|
||||||
|
public final boolean max;
|
||||||
|
|
||||||
|
public final boolean avg;
|
||||||
|
|
||||||
|
public GraphDto(@NonNull final Graph graph) {
|
||||||
|
this.id = graph.getId();
|
||||||
|
this.version = graph.getVersion();
|
||||||
|
this.series = new SeriesDto(graph.getSeries(), false);
|
||||||
|
this.name = graph.getName();
|
||||||
|
this.visible = graph.isVisible();
|
||||||
|
this.color = graph.getColor();
|
||||||
|
this.factor = graph.getFactor();
|
||||||
|
this.group = graph.getGroup();
|
||||||
|
this.stack = graph.getStack();
|
||||||
|
this.min = graph.isMin();
|
||||||
|
this.max = graph.isMax();
|
||||||
|
this.avg = graph.isAvg();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
public interface GraphRepository extends ListCrudRepository<Graph, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
45
src/main/java/de/ph87/data/plot/axis/graph/GraphService.java
Normal file
45
src/main/java/de/ph87/data/plot/axis/graph/GraphService.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.plot.PlotDto;
|
||||||
|
import de.ph87.data.plot.PlotService;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GraphService {
|
||||||
|
|
||||||
|
private final GraphRepository graphRepository;
|
||||||
|
|
||||||
|
private final PlotService plotService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto delete(final long id) {
|
||||||
|
final Graph graph = getById(id);
|
||||||
|
graphRepository.delete(graph);
|
||||||
|
graph.getAxis().getGraphs().remove(graph);
|
||||||
|
log.info("Deleted: graph={}", graph);
|
||||||
|
return plotService.publish(graph.getAxis().getPlot(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PlotDto set(final long id, @NonNull final Consumer<Graph> modifier) {
|
||||||
|
final Graph graph = getById(id);
|
||||||
|
modifier.accept(graph);
|
||||||
|
return plotService.publish(graph.getAxis().getPlot(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Graph getById(final long id) {
|
||||||
|
return graphRepository.findById(id).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/java/de/ph87/data/plot/axis/graph/Group.java
Normal file
13
src/main/java/de/ph87/data/plot/axis/graph/Group.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
public enum Group {
|
||||||
|
NONE,
|
||||||
|
FIVE_OF_DAY,
|
||||||
|
HOUR_OF_DAY,
|
||||||
|
HOUR_OF_WEEK,
|
||||||
|
HOUR_OF_MONTH,
|
||||||
|
DAY_OF_WEEK,
|
||||||
|
DAY_OF_MONTH,
|
||||||
|
DAY_OF_YEAR,
|
||||||
|
WEEK_OF_YEAR,
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ public class SeriesController {
|
|||||||
|
|
||||||
private final SeriesRepository seriesRepository;
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final SeriesService SeriesService;
|
||||||
|
|
||||||
@GetMapping("findAll")
|
@GetMapping("findAll")
|
||||||
public List<SeriesDto> findAll() {
|
public List<SeriesDto> findAll() {
|
||||||
@ -34,7 +34,7 @@ public class SeriesController {
|
|||||||
|
|
||||||
@PostMapping("points")
|
@PostMapping("points")
|
||||||
public List<? extends SeriesPoint> points(@NonNull @RequestBody final SeriesPointsRequest request) {
|
public List<? extends SeriesPoint> points(@NonNull @RequestBody final SeriesPointsRequest request) {
|
||||||
return seriesService.points(request);
|
return SeriesService.points(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,10 @@ public class SeriesDto implements IWebsocketMessage {
|
|||||||
|
|
||||||
public final long id;
|
public final long id;
|
||||||
|
|
||||||
|
public final long version;
|
||||||
|
|
||||||
|
public final boolean deleted;
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -33,8 +37,10 @@ public class SeriesDto implements IWebsocketMessage {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public final SeriesType type;
|
public final SeriesType type;
|
||||||
|
|
||||||
public SeriesDto(@NonNull final Series series) {
|
public SeriesDto(@NonNull final Series series, final boolean deleted) {
|
||||||
this.id = series.getId();
|
this.id = series.getId();
|
||||||
|
this.version = series.getVersion();
|
||||||
|
this.deleted = deleted;
|
||||||
this.name = series.getName();
|
this.name = series.getName();
|
||||||
this.unit = series.getUnit();
|
this.unit = series.getUnit();
|
||||||
this.decimals = series.getDecimals();
|
this.decimals = series.getDecimals();
|
||||||
|
|||||||
@ -13,11 +13,13 @@ public interface SeriesRepository extends ListCrudRepository<Series, Long> {
|
|||||||
Optional<Series> findByName(@NonNull String seriesName);
|
Optional<Series> findByName(@NonNull String seriesName);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Query("select new de.ph87.data.series.SeriesDto(s) from Series s where s.id = :id")
|
@Query("select new de.ph87.data.series.SeriesDto(s, false) from Series s where s.id = :id")
|
||||||
SeriesDto getDtoById(long id);
|
SeriesDto getDtoById(long id);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Query("select new de.ph87.data.series.SeriesDto(t) from Series t")
|
@Query("select new de.ph87.data.series.SeriesDto(t, false) from Series t")
|
||||||
List<SeriesDto> findAllDto();
|
List<SeriesDto> findAllDto();
|
||||||
|
|
||||||
|
Optional<Series> findFirstByOrderByNameAsc();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,17 @@ public class BoolDto implements IWebsocketMessage {
|
|||||||
public final boolean terminated;
|
public final boolean terminated;
|
||||||
|
|
||||||
public BoolDto(@NonNull final Bool bool) {
|
public BoolDto(@NonNull final Bool bool) {
|
||||||
this.series = new SeriesDto(bool.getId().getSeries());
|
this.series = new SeriesDto(bool.getId().getSeries(), false);
|
||||||
this.date = bool.getId().getDate();
|
this.date = bool.getId().getDate();
|
||||||
this.end = bool.getEnd();
|
this.end = bool.getEnd();
|
||||||
this.state = bool.isState();
|
this.state = bool.isState();
|
||||||
this.terminated = bool.isTerminated();
|
this.terminated = bool.isTerminated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getWebsocketTopic() {
|
||||||
|
return "Bool/%d".formatted(series.id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,52 @@
|
|||||||
package de.ph87.data.series.data.delta;
|
package de.ph87.data.series.data.delta;
|
||||||
|
|
||||||
import de.ph87.data.series.data.DataId;
|
import de.ph87.data.series.SeriesDto;
|
||||||
|
import de.ph87.data.series.data.Interval;
|
||||||
import de.ph87.data.websocket.IWebsocketMessage;
|
import de.ph87.data.websocket.IWebsocketMessage;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public abstract class DeltaDto implements IWebsocketMessage {
|
public abstract class DeltaDto implements IWebsocketMessage {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final DataId id;
|
private SeriesDto series;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private ZonedDateTime date;
|
||||||
|
|
||||||
public final double first;
|
public final double first;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final double last;
|
public final double last;
|
||||||
|
|
||||||
protected DeltaDto(@NonNull final Delta delta) {
|
@NonNull
|
||||||
this.id = delta.getId();
|
public final Interval interval;
|
||||||
|
|
||||||
|
protected DeltaDto(@NonNull final Delta delta, @NonNull final Interval interval) {
|
||||||
|
this.series = new SeriesDto(delta.getId().getSeries(), false);
|
||||||
|
this.date = delta.getId().getDate();
|
||||||
this.first = delta.getFirst();
|
this.first = delta.getFirst();
|
||||||
this.last = delta.getLast();
|
this.last = delta.getLast();
|
||||||
|
this.interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getWebsocketTopic() {
|
||||||
|
return "Delta/%d/%s".formatted(series.id, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public static class Five extends DeltaDto {
|
public static class Five extends DeltaDto {
|
||||||
|
|
||||||
public Five(@NonNull final Delta.Five delta) {
|
public Five(@NonNull final Delta.Five delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -38,8 +55,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Hour extends DeltaDto {
|
public static class Hour extends DeltaDto {
|
||||||
|
|
||||||
public Hour(@NonNull final Delta.Hour delta) {
|
public Hour(@NonNull final Delta.Hour delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -48,8 +65,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Day extends DeltaDto {
|
public static class Day extends DeltaDto {
|
||||||
|
|
||||||
public Day(@NonNull final Delta.Day delta) {
|
public Day(@NonNull final Delta.Day delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -58,8 +75,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Week extends DeltaDto {
|
public static class Week extends DeltaDto {
|
||||||
|
|
||||||
public Week(@NonNull final Delta.Week delta) {
|
public Week(@NonNull final Delta.Week delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -68,8 +85,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Month extends DeltaDto {
|
public static class Month extends DeltaDto {
|
||||||
|
|
||||||
public Month(@NonNull final Delta.Month delta) {
|
public Month(@NonNull final Delta.Month delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -78,8 +95,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Year extends DeltaDto {
|
public static class Year extends DeltaDto {
|
||||||
|
|
||||||
public Year(@NonNull final Delta.Year delta) {
|
public Year(@NonNull final Delta.Year delta, @NonNull final Interval interval) {
|
||||||
super(delta);
|
super(delta, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -45,11 +44,11 @@ public class DeltaService {
|
|||||||
write(series, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::new);
|
write(series, year, Interval.YEAR, date, value, Delta.Year::new, DeltaDto.Year::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <DELTA extends Delta, DTO extends DeltaDto> void write(@NonNull final Series series, @NonNull final DeltaRepo<DELTA> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, DELTA> create, @NonNull final Function<DELTA, DTO> toDto) {
|
private <DELTA extends Delta, DTO extends DeltaDto> void write(@NonNull final Series series, @NonNull final DeltaRepo<DELTA> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, DELTA> create, @NonNull final BiFunction<DELTA, Interval, DTO> toDto) {
|
||||||
final DataId id = new DataId(series, date, interval);
|
final DataId id = new DataId(series, date, interval);
|
||||||
final DELTA delta = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
final DELTA delta = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||||
log.debug("Delta written: {}", delta);
|
log.debug("Delta written: {}", delta);
|
||||||
applicationEventPublisher.publishEvent(toDto.apply(delta));
|
applicationEventPublisher.publishEvent(toDto.apply(delta, interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.ph87.data.series.data.varying;
|
package de.ph87.data.series.data.varying;
|
||||||
|
|
||||||
import de.ph87.data.series.SeriesDto;
|
import de.ph87.data.series.SeriesDto;
|
||||||
|
import de.ph87.data.series.data.Interval;
|
||||||
import de.ph87.data.websocket.IWebsocketMessage;
|
import de.ph87.data.websocket.IWebsocketMessage;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -13,7 +14,7 @@ import java.time.ZonedDateTime;
|
|||||||
public abstract class VaryingDto implements IWebsocketMessage {
|
public abstract class VaryingDto implements IWebsocketMessage {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final SeriesDto seriesDto;
|
public final SeriesDto series;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final ZonedDateTime date;
|
public final ZonedDateTime date;
|
||||||
@ -26,21 +27,31 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
|
|
||||||
public final int count;
|
public final int count;
|
||||||
|
|
||||||
protected VaryingDto(@NonNull final Varying varying) {
|
@NonNull
|
||||||
this.seriesDto = new SeriesDto(varying.getId().getSeries());
|
public final Interval interval;
|
||||||
|
|
||||||
|
protected VaryingDto(@NonNull final Varying varying, @NonNull final Interval interval) {
|
||||||
|
this.series = new SeriesDto(varying.getId().getSeries(), false);
|
||||||
this.date = varying.getId().getDate();
|
this.date = varying.getId().getDate();
|
||||||
this.min = varying.getMin();
|
this.min = varying.getMin();
|
||||||
this.max = varying.getMax();
|
this.max = varying.getMax();
|
||||||
this.avg = varying.getAvg();
|
this.avg = varying.getAvg();
|
||||||
this.count = varying.getCount();
|
this.count = varying.getCount();
|
||||||
|
this.interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getWebsocketTopic() {
|
||||||
|
return "Varying/%d/%s".formatted(series.id, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public static class Five extends VaryingDto {
|
public static class Five extends VaryingDto {
|
||||||
|
|
||||||
public Five(@NonNull final Varying.Five varying) {
|
public Five(@NonNull final Varying.Five varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -49,8 +60,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Hour extends VaryingDto {
|
public static class Hour extends VaryingDto {
|
||||||
|
|
||||||
public Hour(@NonNull final Varying.Hour varying) {
|
public Hour(@NonNull final Varying.Hour varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -59,8 +70,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Day extends VaryingDto {
|
public static class Day extends VaryingDto {
|
||||||
|
|
||||||
public Day(@NonNull final Varying.Day varying) {
|
public Day(@NonNull final Varying.Day varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -69,8 +80,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Week extends VaryingDto {
|
public static class Week extends VaryingDto {
|
||||||
|
|
||||||
public Week(@NonNull final Varying.Week varying) {
|
public Week(@NonNull final Varying.Week varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -79,8 +90,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Month extends VaryingDto {
|
public static class Month extends VaryingDto {
|
||||||
|
|
||||||
public Month(@NonNull final Varying.Month varying) {
|
public Month(@NonNull final Varying.Month varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -89,8 +100,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
|||||||
@ToString
|
@ToString
|
||||||
public static class Year extends VaryingDto {
|
public static class Year extends VaryingDto {
|
||||||
|
|
||||||
public Year(@NonNull final Varying.Year varying) {
|
public Year(@NonNull final Varying.Year varying, @NonNull final Interval interval) {
|
||||||
super(varying);
|
super(varying, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -45,11 +44,11 @@ public class VaryingService {
|
|||||||
write(series, year, Interval.YEAR, date, value, Varying.Year::new, VaryingDto.Year::new);
|
write(series, year, Interval.YEAR, date, value, Varying.Year::new, VaryingDto.Year::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <VARYING extends Varying, DTO extends VaryingDto> void write(@NonNull final Series series, @NonNull final VaryingRepo<VARYING> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, VARYING> create, @NonNull final Function<VARYING, DTO> toDto) {
|
private <VARYING extends Varying, DTO extends VaryingDto> void write(@NonNull final Series series, @NonNull final VaryingRepo<VARYING> repo, @NonNull final Interval interval, @NonNull final ZonedDateTime date, final double value, @NonNull final BiFunction<DataId, Double, VARYING> create, @NonNull final BiFunction<VARYING, Interval, DTO> toDto) {
|
||||||
final DataId id = new DataId(series, date, interval);
|
final DataId id = new DataId(series, date, interval);
|
||||||
final VARYING varying = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
final VARYING varying = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||||
log.debug("Varying written: {}", varying);
|
log.debug("Varying written: {}", varying);
|
||||||
applicationEventPublisher.publishEvent(toDto.apply(varying));
|
applicationEventPublisher.publishEvent(toDto.apply(varying, interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
@ -102,7 +102,7 @@ public class TopicReceiver {
|
|||||||
v -> {
|
v -> {
|
||||||
final double value = query.getFunction().apply(v) * query.getFactor();
|
final double value = query.getFunction().apply(v) * query.getFactor();
|
||||||
series.update(date, value);
|
series.update(date, value);
|
||||||
applicationEventPublisher.publishEvent(new SeriesDto(series));
|
applicationEventPublisher.publishEvent(new SeriesDto(series, false));
|
||||||
|
|
||||||
switch (series.getType()) {
|
switch (series.getType()) {
|
||||||
case BOOL -> {
|
case BOOL -> {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public class TopicQueryDto {
|
|||||||
public final double factor;
|
public final double factor;
|
||||||
|
|
||||||
public TopicQueryDto(@NonNull final TopicQuery topicQuery) {
|
public TopicQueryDto(@NonNull final TopicQuery topicQuery) {
|
||||||
this.series = map(topicQuery.getSeries(), SeriesDto::new);
|
this.series = map(topicQuery.getSeries(), series -> new SeriesDto(series, false));
|
||||||
this.valueQuery = topicQuery.getValueQuery();
|
this.valueQuery = topicQuery.getValueQuery();
|
||||||
this.beginQuery = topicQuery.getBeginQuery();
|
this.beginQuery = topicQuery.getBeginQuery();
|
||||||
this.terminatedQuery = topicQuery.getTerminatedQuery();
|
this.terminatedQuery = topicQuery.getTerminatedQuery();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user