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/platform-browser": "^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/stompjs": "^7.2.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
@ -1397,6 +1400,64 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz",
|
||||
|
||||
@ -28,6 +28,9 @@
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^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/stompjs": "^7.2.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
|
||||
@ -5,6 +5,7 @@ import {Injectable} from '@angular/core';
|
||||
import {RxStompState} from '@stomp/rx-stomp';
|
||||
|
||||
export type FromJson<T> = (json: any) => T;
|
||||
export type FromJsonIndexed<T> = (json: any, index: number) => T;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function validateListIndexed<T>(json: any, fromJson: FromJsonIndexed<T>): T[] {
|
||||
return json.map(fromJson);
|
||||
}
|
||||
|
||||
export function url(protocol: string, path: any[]): string {
|
||||
const secure = location.protocol.endsWith('s:') ? 's' : '';
|
||||
return `${protocol}${secure}://${location.hostname}:8080/${path.join('/')}`;
|
||||
@ -116,19 +121,19 @@ export class ApiService {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -142,7 +147,7 @@ export class ApiService {
|
||||
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
|
||||
.subscribe(topic.join("/"))
|
||||
.pipe(
|
||||
@ -169,22 +174,36 @@ export abstract class CrudService<T> {
|
||||
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);
|
||||
}
|
||||
|
||||
protected getList(path: any[], next: Next<T[]>) {
|
||||
protected getList(path: any[], next?: Next<T[]>) {
|
||||
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);
|
||||
}
|
||||
|
||||
subscribe(next: Next<T>): Subscription {
|
||||
subscribe(next: Next<T>, path: any[] = []): 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {stompServiceFactory} from './COMMON';
|
||||
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 = {
|
||||
providers: [
|
||||
|
||||
@ -1,3 +1 @@
|
||||
<app-plot></app-plot>
|
||||
|
||||
<router-outlet/>
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
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 {RouterOutlet} from '@angular/router';
|
||||
import {Plot} from './plot/plot';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, Plot],
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.html',
|
||||
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 {
|
||||
|
||||
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(
|
||||
readonly name: string,
|
||||
readonly display: string,
|
||||
readonly spanGaps: number,
|
||||
) {
|
||||
Interval.values.push(this);
|
||||
Interval._values.push(this);
|
||||
}
|
||||
|
||||
static fromJson(json: any): Interval {
|
||||
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) {
|
||||
throw new Error(`Not an Interval: ${JSON.stringify(json)}`);
|
||||
}
|
||||
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 {formatNumber} from '@angular/common';
|
||||
|
||||
export class Series {
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly version: number,
|
||||
readonly deleted: boolean,
|
||||
readonly name: string,
|
||||
readonly unit: string,
|
||||
readonly type: SeriesType,
|
||||
readonly decimals: number,
|
||||
readonly value: number | null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@ -15,10 +20,30 @@ export class Series {
|
||||
static fromJson(json: any): Series {
|
||||
return new Series(
|
||||
validateNumber(json.id),
|
||||
validateNumber(json.version),
|
||||
validateBoolean(json.deleted),
|
||||
validateString(json.name),
|
||||
validateString(json.unit),
|
||||
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%;
|
||||
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;
|
||||
|
||||
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.SeriesRepository;
|
||||
import de.ph87.data.series.SeriesType;
|
||||
@ -26,9 +32,27 @@ public class DemoService {
|
||||
|
||||
private final TopicRepository topicRepository;
|
||||
|
||||
private final PlotRepository plotRepository;
|
||||
|
||||
private final AxisRepository axisRepository;
|
||||
|
||||
private final GraphRepository graphRepository;
|
||||
|
||||
@Transactional
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
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);
|
||||
topic(
|
||||
"Infrarotheizung",
|
||||
@ -108,6 +132,69 @@ public class DemoService {
|
||||
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
|
||||
private Series series(@NonNull final String name, @NonNull final String unit, @NonNull final SeriesType type, final int expectedEverySeconds) {
|
||||
return seriesRepository
|
||||
|
||||
@ -15,4 +15,12 @@ public class Helpers {
|
||||
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.persistence.ElementCollection;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.OrderColumn;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
import org.slf4j.Logger;
|
||||
@ -16,9 +14,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@MappedSuperclass
|
||||
@NoArgsConstructor
|
||||
public abstract class AbstractEntityLog {
|
||||
|
||||
@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 SeriesService seriesService;
|
||||
private final SeriesService SeriesService;
|
||||
|
||||
@GetMapping("findAll")
|
||||
public List<SeriesDto> findAll() {
|
||||
@ -34,7 +34,7 @@ public class SeriesController {
|
||||
|
||||
@PostMapping("points")
|
||||
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 version;
|
||||
|
||||
public final boolean deleted;
|
||||
|
||||
public final String name;
|
||||
|
||||
@NonNull
|
||||
@ -33,8 +37,10 @@ public class SeriesDto implements IWebsocketMessage {
|
||||
@NonNull
|
||||
public final SeriesType type;
|
||||
|
||||
public SeriesDto(@NonNull final Series series) {
|
||||
public SeriesDto(@NonNull final Series series, final boolean deleted) {
|
||||
this.id = series.getId();
|
||||
this.version = series.getVersion();
|
||||
this.deleted = deleted;
|
||||
this.name = series.getName();
|
||||
this.unit = series.getUnit();
|
||||
this.decimals = series.getDecimals();
|
||||
|
||||
@ -13,11 +13,13 @@ public interface SeriesRepository extends ListCrudRepository<Series, Long> {
|
||||
Optional<Series> findByName(@NonNull String seriesName);
|
||||
|
||||
@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);
|
||||
|
||||
@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();
|
||||
|
||||
Optional<Series> findFirstByOrderByNameAsc();
|
||||
|
||||
}
|
||||
|
||||
@ -24,11 +24,17 @@ public class BoolDto implements IWebsocketMessage {
|
||||
public final boolean terminated;
|
||||
|
||||
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.end = bool.getEnd();
|
||||
this.state = bool.isState();
|
||||
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;
|
||||
|
||||
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 lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
public abstract class DeltaDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final DataId id;
|
||||
private SeriesDto series;
|
||||
|
||||
@NonNull
|
||||
private ZonedDateTime date;
|
||||
|
||||
public final double first;
|
||||
|
||||
@NonNull
|
||||
public final double last;
|
||||
|
||||
protected DeltaDto(@NonNull final Delta delta) {
|
||||
this.id = delta.getId();
|
||||
@NonNull
|
||||
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.last = delta.getLast();
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getWebsocketTopic() {
|
||||
return "Delta/%d/%s".formatted(series.id, interval);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Five extends DeltaDto {
|
||||
|
||||
public Five(@NonNull final Delta.Five delta) {
|
||||
super(delta);
|
||||
public Five(@NonNull final Delta.Five delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -38,8 +55,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Hour extends DeltaDto {
|
||||
|
||||
public Hour(@NonNull final Delta.Hour delta) {
|
||||
super(delta);
|
||||
public Hour(@NonNull final Delta.Hour delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -48,8 +65,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Day extends DeltaDto {
|
||||
|
||||
public Day(@NonNull final Delta.Day delta) {
|
||||
super(delta);
|
||||
public Day(@NonNull final Delta.Day delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -58,8 +75,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Week extends DeltaDto {
|
||||
|
||||
public Week(@NonNull final Delta.Week delta) {
|
||||
super(delta);
|
||||
public Week(@NonNull final Delta.Week delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -68,8 +85,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Month extends DeltaDto {
|
||||
|
||||
public Month(@NonNull final Delta.Month delta) {
|
||||
super(delta);
|
||||
public Month(@NonNull final Delta.Month delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -78,8 +95,8 @@ public abstract class DeltaDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Year extends DeltaDto {
|
||||
|
||||
public Year(@NonNull final Delta.Year delta) {
|
||||
super(delta);
|
||||
public Year(@NonNull final Delta.Year delta, @NonNull final Interval interval) {
|
||||
super(delta, interval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -45,11 +44,11 @@ public class DeltaService {
|
||||
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 DELTA delta = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||
log.debug("Delta written: {}", delta);
|
||||
applicationEventPublisher.publishEvent(toDto.apply(delta));
|
||||
applicationEventPublisher.publishEvent(toDto.apply(delta, interval));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.ph87.data.series.data.varying;
|
||||
|
||||
import de.ph87.data.series.SeriesDto;
|
||||
import de.ph87.data.series.data.Interval;
|
||||
import de.ph87.data.websocket.IWebsocketMessage;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@ -13,7 +14,7 @@ import java.time.ZonedDateTime;
|
||||
public abstract class VaryingDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final SeriesDto seriesDto;
|
||||
public final SeriesDto series;
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date;
|
||||
@ -26,21 +27,31 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
|
||||
public final int count;
|
||||
|
||||
protected VaryingDto(@NonNull final Varying varying) {
|
||||
this.seriesDto = new SeriesDto(varying.getId().getSeries());
|
||||
@NonNull
|
||||
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.min = varying.getMin();
|
||||
this.max = varying.getMax();
|
||||
this.avg = varying.getAvg();
|
||||
this.count = varying.getCount();
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getWebsocketTopic() {
|
||||
return "Varying/%d/%s".formatted(series.id, interval);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public static class Five extends VaryingDto {
|
||||
|
||||
public Five(@NonNull final Varying.Five varying) {
|
||||
super(varying);
|
||||
public Five(@NonNull final Varying.Five varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -49,8 +60,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Hour extends VaryingDto {
|
||||
|
||||
public Hour(@NonNull final Varying.Hour varying) {
|
||||
super(varying);
|
||||
public Hour(@NonNull final Varying.Hour varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -59,8 +70,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Day extends VaryingDto {
|
||||
|
||||
public Day(@NonNull final Varying.Day varying) {
|
||||
super(varying);
|
||||
public Day(@NonNull final Varying.Day varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -69,8 +80,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Week extends VaryingDto {
|
||||
|
||||
public Week(@NonNull final Varying.Week varying) {
|
||||
super(varying);
|
||||
public Week(@NonNull final Varying.Week varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -79,8 +90,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Month extends VaryingDto {
|
||||
|
||||
public Month(@NonNull final Varying.Month varying) {
|
||||
super(varying);
|
||||
public Month(@NonNull final Varying.Month varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
@ -89,8 +100,8 @@ public abstract class VaryingDto implements IWebsocketMessage {
|
||||
@ToString
|
||||
public static class Year extends VaryingDto {
|
||||
|
||||
public Year(@NonNull final Varying.Year varying) {
|
||||
super(varying);
|
||||
public Year(@NonNull final Varying.Year varying, @NonNull final Interval interval) {
|
||||
super(varying, interval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -45,11 +44,11 @@ public class VaryingService {
|
||||
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 VARYING varying = repo.findById(id).stream().peek(existing -> existing.update(value)).findFirst().orElseGet(() -> repo.save(create.apply(id, value)));
|
||||
log.debug("Varying written: {}", varying);
|
||||
applicationEventPublisher.publishEvent(toDto.apply(varying));
|
||||
applicationEventPublisher.publishEvent(toDto.apply(varying, interval));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@ -102,7 +102,7 @@ public class TopicReceiver {
|
||||
v -> {
|
||||
final double value = query.getFunction().apply(v) * query.getFactor();
|
||||
series.update(date, value);
|
||||
applicationEventPublisher.publishEvent(new SeriesDto(series));
|
||||
applicationEventPublisher.publishEvent(new SeriesDto(series, false));
|
||||
|
||||
switch (series.getType()) {
|
||||
case BOOL -> {
|
||||
|
||||
@ -30,7 +30,7 @@ public class TopicQueryDto {
|
||||
public final double factor;
|
||||
|
||||
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.beginQuery = topicQuery.getBeginQuery();
|
||||
this.terminatedQuery = topicQuery.getTerminatedQuery();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user