From 54e14873007d7b6c0d0e67f64bd23feecfbc4ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Tue, 6 May 2025 12:34:19 +0200 Subject: [PATCH] View backend + readonly frontend --- pom.xml | 2 +- src/main/angular/colors.less | 1 + src/main/angular/src/app/View/View.ts | 155 ++++++++++++++++++ .../View/view-body/view-body.component.html | 49 ++++++ .../View/view-body/view-body.component.less | 19 +++ .../app/View/view-body/view-body.component.ts | 40 +++++ .../View/view-list/view-list.component.html | 11 ++ .../View/view-list/view-list.component.less | 24 +++ .../app/View/view-list/view-list.component.ts | 46 ++++++ src/main/angular/src/app/View/view.service.ts | 28 ++++ src/main/angular/src/app/app.component.less | 6 +- src/main/angular/src/app/app.routes.ts | 3 + src/main/angular/src/app/core/api.service.ts | 2 +- src/main/angular/src/styles.less | 8 + .../ph87/data/{series => }/graph/Graph.java | 44 ++--- .../de/ph87/data/graph/GraphController.java | 49 ++++++ .../GraphPoint.java => point/Point.java} | 8 +- .../de/ph87/data/point/PointController.java | 26 +++ .../java/de/ph87/data/point/PointRequest.java | 41 +++++ .../java/de/ph87/data/point/PointService.java | 30 ++++ .../java/de/ph87/data/series/SeriesDto.java | 2 + .../de/ph87/data/series/SeriesService.java | 23 ++- .../data/series/graph/GraphController.java | 40 ----- .../ph87/data/series/graph/GraphService.java | 37 ----- .../ph87/data/series/meter/MeterService.java | 47 +++--- .../data/series/varying/VaryingService.java | 12 +- .../de/ph87/data/view/ViewController.java | 23 +++ .../de/ph87/data/view/ViewPointRequest.java | 20 +++ .../de/ph87/data/view/ViewRepository.java | 20 +++ .../java/de/ph87/data/view/ViewScope.java | 46 ++++++ .../java/de/ph87/data/view/ViewService.java | 58 +++++++ .../java/de/ph87/data/view/tree/View.java | 37 +++++ .../de/ph87/data/view/tree/ViewBinary.java | 84 ++++++++++ .../de/ph87/data/view/tree/ViewBinaryDto.java | 27 +++ .../java/de/ph87/data/view/tree/ViewDto.java | 34 ++++ .../de/ph87/data/view/tree/ViewLiteral.java | 32 ++++ .../ph87/data/view/tree/ViewLiteralDto.java | 18 ++ .../de/ph87/data/view/tree/ViewSeries.java | 34 ++++ .../de/ph87/data/view/tree/ViewSeriesDto.java | 20 +++ .../de/ph87/data/view/tree/ViewUnary.java | 56 +++++++ .../de/ph87/data/view/tree/ViewUnaryDto.java | 23 +++ .../de/ph87/data/weather/WeatherService.java | 2 +- 42 files changed, 1136 insertions(+), 151 deletions(-) create mode 100644 src/main/angular/src/app/View/View.ts create mode 100644 src/main/angular/src/app/View/view-body/view-body.component.html create mode 100644 src/main/angular/src/app/View/view-body/view-body.component.less create mode 100644 src/main/angular/src/app/View/view-body/view-body.component.ts create mode 100644 src/main/angular/src/app/View/view-list/view-list.component.html create mode 100644 src/main/angular/src/app/View/view-list/view-list.component.less create mode 100644 src/main/angular/src/app/View/view-list/view-list.component.ts create mode 100644 src/main/angular/src/app/View/view.service.ts rename src/main/java/de/ph87/data/{series => }/graph/Graph.java (81%) create mode 100644 src/main/java/de/ph87/data/graph/GraphController.java rename src/main/java/de/ph87/data/{series/graph/GraphPoint.java => point/Point.java} (63%) create mode 100644 src/main/java/de/ph87/data/point/PointController.java create mode 100644 src/main/java/de/ph87/data/point/PointRequest.java create mode 100644 src/main/java/de/ph87/data/point/PointService.java delete mode 100644 src/main/java/de/ph87/data/series/graph/GraphController.java delete mode 100644 src/main/java/de/ph87/data/series/graph/GraphService.java create mode 100644 src/main/java/de/ph87/data/view/ViewController.java create mode 100644 src/main/java/de/ph87/data/view/ViewPointRequest.java create mode 100644 src/main/java/de/ph87/data/view/ViewRepository.java create mode 100644 src/main/java/de/ph87/data/view/ViewScope.java create mode 100644 src/main/java/de/ph87/data/view/ViewService.java create mode 100644 src/main/java/de/ph87/data/view/tree/View.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewBinary.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewDto.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewLiteral.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewSeries.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewUnary.java create mode 100644 src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java diff --git a/pom.xml b/pom.xml index bce4387..c7f3cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.springframework.boot spring-boot-starter-parent - 3.4.2 + 3.4.4 diff --git a/src/main/angular/colors.less b/src/main/angular/colors.less index d3832e6..a28e0c0 100644 --- a/src/main/angular/colors.less +++ b/src/main/angular/colors.less @@ -2,6 +2,7 @@ @foreground: gray; @background: white; +@FONT_SELECTABLE: white; @consumption: orange; @purchase: orangered; diff --git a/src/main/angular/src/app/View/View.ts b/src/main/angular/src/app/View/View.ts new file mode 100644 index 0000000..24597bf --- /dev/null +++ b/src/main/angular/src/app/View/View.ts @@ -0,0 +1,155 @@ +import {Series} from '../series/Series'; +import {validateNumber, validateString} from '../core/validators'; + +export abstract class View { + + protected constructor( + readonly _type_: string, + readonly uuid: string, + readonly name: string, + ) { + // + } + + static fromJson(json: any, locale: string): View { + const type = validateString(json._type_); + switch (type) { + case 'literal': + return ViewLiteral.fromJson2(json); + case 'series': + return ViewSeries.fromJson2(json, locale); + case 'unary': + return ViewUnary.fromJson2(json, locale); + case 'binary': + return ViewBinary.fromJson2(json, locale); + default: + throw new Error(`View type '${type}' not implemented.`); + } + } + +} + +export class ViewLiteral extends View { + + constructor( + _type_: string, + uuid: string, + name: string, + readonly value: number, + ) { + super(_type_, uuid, name); + } + + static fromJson2(json: any): ViewLiteral { + return new ViewLiteral( + validateString(json._type_), + validateString(json.uuid), + validateString(json.name), + validateNumber(json.value), + ); + } + + static cast(view: View): ViewLiteral { + return view as ViewLiteral; + } + +} + +export class ViewSeries extends View { + + constructor( + _type_: string, + uuid: string, + name: string, + readonly series: Series, + ) { + super(_type_, uuid, name); + } + + static fromJson2(json: any, locale: string): ViewSeries { + return new ViewSeries( + validateString(json._type_), + validateString(json.uuid), + validateString(json.name), + Series.fromJson(json.series, locale), + ); + } + + static cast(view: View): ViewSeries { + return view as ViewSeries; + } + +} + +export enum ViewUnaryOperator { + NEG = "NEG", + REC = "REC", + NOT_NEG = "NOT_NEG", +} + +export class ViewUnary extends View { + + constructor( + _type_: string, + uuid: string, + name: string, + readonly operation: ViewUnaryOperator, + readonly view: View, + ) { + super(_type_, uuid, name); + } + + static fromJson2(json: any, locale: string): ViewUnary { + return new ViewUnary( + validateString(json._type_), + validateString(json.uuid), + validateString(json.name), + validateString(json.operation) as ViewUnaryOperator, + View.fromJson(json.view, locale), + ); + } + + static cast(view: View): ViewUnary { + return view as ViewUnary; + } + +} + +export enum ViewBinaryOperator { + PLUS = 'PLUS', + MINUS = 'MINUS', + MULTIPLY = 'MULTIPLY', + DIVIDE = 'DIVIDE', + MODULO = 'MODULO', + PERCENT = 'PERCENT', +} + +export class ViewBinary extends View { + + constructor( + _type_: string, + uuid: string, + name: string, + readonly operation: ViewBinaryOperator, + readonly view0: View, + readonly view1: View, + ) { + super(_type_, uuid, name); + } + + static fromJson2(json: any, locale: string): ViewBinary { + return new ViewBinary( + validateString(json._type_), + validateString(json.uuid), + validateString(json.name), + validateString(json.operation) as ViewBinaryOperator, + View.fromJson(json.view0, locale), + View.fromJson(json.view1, locale), + ); + } + + static cast(view: View): ViewBinary { + return view as ViewBinary; + } + +} diff --git a/src/main/angular/src/app/View/view-body/view-body.component.html b/src/main/angular/src/app/View/view-body/view-body.component.html new file mode 100644 index 0000000..465b624 --- /dev/null +++ b/src/main/angular/src/app/View/view-body/view-body.component.html @@ -0,0 +1,49 @@ + + +
+ + + + + + + + + + + +
+
+ +
+
+
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ +
diff --git a/src/main/angular/src/app/View/view-body/view-body.component.less b/src/main/angular/src/app/View/view-body/view-body.component.less new file mode 100644 index 0000000..0b1a0b8 --- /dev/null +++ b/src/main/angular/src/app/View/view-body/view-body.component.less @@ -0,0 +1,19 @@ +@import "../../../../colors"; + +.children { + margin-left: 0.5em; + border-left: 0.1em solid green; + overflow: visible; + + .child { + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: 0.5em; + } + +} + +select.binary { + background-color: @background; + margin-left: -0.5em; +} diff --git a/src/main/angular/src/app/View/view-body/view-body.component.ts b/src/main/angular/src/app/View/view-body/view-body.component.ts new file mode 100644 index 0000000..b0a0f2b --- /dev/null +++ b/src/main/angular/src/app/View/view-body/view-body.component.ts @@ -0,0 +1,40 @@ +import {Component, Input} from '@angular/core'; +import {NgForOf, NgIf, NgSwitch, NgSwitchCase} from '@angular/common'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {View, ViewBinary, ViewBinaryOperator, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View'; +import {Series} from '../../series/Series'; + +@Component({ + selector: 'app-view-body', + imports: [ + NgForOf, + NgSwitchCase, + ReactiveFormsModule, + NgSwitch, + FormsModule, + NgIf + ], + templateUrl: './view-body.component.html', + styleUrl: './view-body.component.less' +}) +export class ViewBodyComponent { + + protected readonly ViewSeries = ViewSeries; + + protected readonly ViewLiteral = ViewLiteral; + + protected readonly ViewUnary = ViewUnary; + + protected readonly ViewUnaryOperator = ViewUnaryOperator; + + protected readonly ViewBinaryOperator = ViewBinaryOperator; + + protected readonly ViewBinary = ViewBinary; + + @Input() + view?: View; + + @Input() + seriesList?: Series[] = []; + +} diff --git a/src/main/angular/src/app/View/view-list/view-list.component.html b/src/main/angular/src/app/View/view-list/view-list.component.html new file mode 100644 index 0000000..b0d49b7 --- /dev/null +++ b/src/main/angular/src/app/View/view-list/view-list.component.html @@ -0,0 +1,11 @@ +
+
+
+
Name:
+ +
+
+ +
+
+
diff --git a/src/main/angular/src/app/View/view-list/view-list.component.less b/src/main/angular/src/app/View/view-list/view-list.component.less new file mode 100644 index 0000000..94a8f68 --- /dev/null +++ b/src/main/angular/src/app/View/view-list/view-list.component.less @@ -0,0 +1,24 @@ +.view { + font-size: 80%; + + .labelPair { + display: flex; + white-space: nowrap; + margin: 0.5em; + + .name { + padding-right: 0.5em; + } + + input { + flex-grow: 1; + width: 0; + } + + } + + .body { + margin: 0.5em; + } + +} diff --git a/src/main/angular/src/app/View/view-list/view-list.component.ts b/src/main/angular/src/app/View/view-list/view-list.component.ts new file mode 100644 index 0000000..7c55600 --- /dev/null +++ b/src/main/angular/src/app/View/view-list/view-list.component.ts @@ -0,0 +1,46 @@ +import {Component, OnInit} from '@angular/core'; +import {ViewService} from '../view.service'; +import {View, ViewLiteral, ViewSeries, ViewUnary, ViewUnaryOperator} from '../View'; +import {NgForOf} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {Series} from '../../series/Series'; +import {SeriesService} from '../../series/series.service'; +import {ViewBodyComponent} from '../view-body/view-body.component'; + +@Component({ + selector: 'app-view-list', + imports: [ + NgForOf, + FormsModule, + ViewBodyComponent + ], + templateUrl: './view-list.component.html', + styleUrl: './view-list.component.less' +}) +export class ViewListComponent implements OnInit { + + protected readonly ViewUnary = ViewUnary; + + protected readonly ViewUnaryOperator = ViewUnaryOperator; + + protected readonly ViewLiteral = ViewLiteral; + + protected readonly ViewSeries = ViewSeries; + + protected seriesList: Series[] = []; + + protected rootList: View[] = []; + + constructor( + readonly viewService: ViewService, + readonly seriesService: SeriesService, + ) { + // + } + + ngOnInit(): void { + this.viewService.list(list => this.rootList = list); + this.seriesService.all(list => this.seriesList = list); + } + +} diff --git a/src/main/angular/src/app/View/view.service.ts b/src/main/angular/src/app/View/view.service.ts new file mode 100644 index 0000000..683ef23 --- /dev/null +++ b/src/main/angular/src/app/View/view.service.ts @@ -0,0 +1,28 @@ +import {Inject, Injectable, LOCALE_ID} from '@angular/core'; +import {ApiService} from '../core/api.service'; +import {FromJson, Next} from '../core/types'; +import {View} from './View'; + +@Injectable({ + providedIn: 'root' +}) +export class ViewService { + + readonly fromJson: FromJson = json => View.fromJson(json, this.locale); + + constructor( + readonly api: ApiService, + @Inject(LOCALE_ID) readonly locale: string, + ) { + // + } + + list(next: Next) { + return this.api.getList(['View', 'rootList'], this.fromJson, next); + } + + byUuid(uuid: string, next: Next) { + return this.api.getSingle(['View', 'byUuid', uuid], this.fromJson, next); + } + +} diff --git a/src/main/angular/src/app/app.component.less b/src/main/angular/src/app/app.component.less index 69a3009..cb183f5 100644 --- a/src/main/angular/src/app/app.component.less +++ b/src/main/angular/src/app/app.component.less @@ -1,5 +1,5 @@ .menubar { - border-bottom: 1px solid black; + border-bottom: 0.05em solid black; background-color: #303d47; .menuitem { @@ -8,12 +8,12 @@ .menuitemLeft { float: left; - border-right: 1px solid black; + border-right: 0.05em solid black; } .menuitemRight { float: right; - border-left: 1px solid black; + border-left: 0.05em solid black; } .menuitemActive { diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index 6d1a114..9c1b5ce 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -2,6 +2,7 @@ import {Routes} from '@angular/router'; import {LiveComponent} from './live/live.component'; import {GreenhouseComponent} from './live/greenhouse/greenhouse/greenhouse.component'; import {HistoryComponent} from './history/history.component'; +import {ViewListComponent} from './View/view-list/view-list.component'; export class Path { @@ -22,6 +23,7 @@ export class Path { export const ROUTING = { LIVE: new Path('Live', 'Live', true), HISTORY: new Path('History', 'Historie', true), + VIEW_LIST: new Path('ViewList', 'Ansichten', true), GREENHOUSE: new Path('Greenhouse', 'Gewächshaus', false), } @@ -33,5 +35,6 @@ export const routes: Routes = [ {path: ROUTING.LIVE.path, component: LiveComponent}, {path: ROUTING.HISTORY.path, component: HistoryComponent}, {path: ROUTING.GREENHOUSE.path, component: GreenhouseComponent}, + {path: ROUTING.VIEW_LIST.path, component: ViewListComponent}, {path: '**', redirectTo: ROUTING.LIVE.path}, ]; diff --git a/src/main/angular/src/app/core/api.service.ts b/src/main/angular/src/app/core/api.service.ts index 49ff931..b6d864a 100644 --- a/src/main/angular/src/app/core/api.service.ts +++ b/src/main/angular/src/app/core/api.service.ts @@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs'; import {StompService} from '@stomp/ng2-stompjs'; import {FromJson, Next} from './types'; -const DEV_TO_PROD = true; +const DEV_TO_PROD = false; @Injectable({ providedIn: 'root' diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 8c0fc8c..66ff44c 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -10,6 +10,14 @@ body { margin: 0; } +input, select { + background-color: transparent; + font-size: inherit; + color: @FONT_SELECTABLE; + border: 0.05em solid gray; + border-radius: 0.2em; +} + button { all: unset; font-size: inherit; diff --git a/src/main/java/de/ph87/data/series/graph/Graph.java b/src/main/java/de/ph87/data/graph/Graph.java similarity index 81% rename from src/main/java/de/ph87/data/series/graph/Graph.java rename to src/main/java/de/ph87/data/graph/Graph.java index fe5ef51..1be78a0 100644 --- a/src/main/java/de/ph87/data/series/graph/Graph.java +++ b/src/main/java/de/ph87/data/graph/Graph.java @@ -1,7 +1,7 @@ -package de.ph87.data.series.graph; +package de.ph87.data.graph; -import de.ph87.data.series.Aligned; -import de.ph87.data.series.Alignment; +import de.ph87.data.point.Point; +import de.ph87.data.point.PointRequest; import de.ph87.data.series.SeriesDto; import de.ph87.data.series.SeriesType; import de.ph87.data.value.Autoscale; @@ -26,13 +26,7 @@ public class Graph { public final SeriesDto series; @NonNull - public final Alignment innerAlignment; - - @NonNull - public final Aligned begin; - - @NonNull - public final Aligned end; + public final PointRequest request; public final int width; @@ -40,7 +34,7 @@ public class Graph { public final int border; - public final List points; + public final List points; public final long minuteMin; @@ -68,11 +62,9 @@ public class Graph { public final Autoscale autoscale; - public Graph(@NonNull final SeriesDto series, @NonNull final List points, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) { + public Graph(@NonNull final SeriesDto series, @NonNull final List points, @NonNull final PointRequest request, final int width, final int height, final int border) { this.series = series; - this.innerAlignment = innerAlignment; - this.begin = begin; - this.end = end; + this.request = request; this.width = width; this.height = height; this.border = border; @@ -81,7 +73,7 @@ public class Graph { double vSum = 0; double vMin = series.getYMin() == null || Double.isNaN(series.getYMin()) ? Double.MIN_VALUE : series.getYMin(); double vMax = series.getYMax() == null || Double.isNaN(series.getYMax()) ? Double.MAX_VALUE : series.getYMax(); - for (final GraphPoint point : points) { + for (final Point point : points) { vMin = Math.min(vMin, point.getValue()); vMax = max(vMax, point.getValue()); vSum += point.getValue(); @@ -96,7 +88,7 @@ public class Graph { // find max label width int __maxLabelWidth = 0; final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics(); - for (final GraphPoint point : points) { + for (final Point point : points) { __maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor))); } this.maxLabelWidth = __maxLabelWidth; @@ -104,10 +96,10 @@ public class Graph { widthInner = width - 3 * border - this.maxLabelWidth; heightInner = height - 2 * border; - minuteMin = begin.date.toEpochSecond() / 60; - minuteMax = end.date.toEpochSecond() / 60; + minuteMin = request.begin.date.toEpochSecond() / 60; + minuteMax = request.end.date.toEpochSecond() / 60; minuteRange = minuteMax - minuteMin; - minuteScale = (double) widthInner / (minuteRange + innerAlignment.maxDuration.toMinutes()); + minuteScale = (double) widthInner / (minuteRange + request.inner.maxDuration.toMinutes()); valueMin = vMin; valueMax = vMax; @@ -137,14 +129,14 @@ public class Graph { g.setColor(Color.WHITE); if (series.type == SeriesType.METER) { - final int space = (int) (minuteScale * innerAlignment.maxDuration.toMinutes()); + final int space = (int) (minuteScale * request.inner.maxDuration.toMinutes()); final int width = (int) (space * 0.95); - for (final Point point : points) { + for (final java.awt.Point point : points) { g.fillRect(point.x + (space - width), 0, width, point.y); } } else { - Point last = null; - for (final Point current : points) { + java.awt.Point last = null; + for (final java.awt.Point current : points) { if (last != null) { g.drawLine(last.x, last.y, current.x, current.y); } @@ -167,7 +159,7 @@ public class Graph { } @NonNull - private Function toPoint() { + private Function toPoint() { return point -> { final long minuteEpoch = point.getDate().toEpochSecond() / 60; final long minuteRelative = minuteEpoch - minuteMin; @@ -178,7 +170,7 @@ public class Graph { final double valueScaled = valueRelative * valueScale; final int y = (int) Math.round(valueScaled); - return new Point(x, y); + return new java.awt.Point(x, y); }; } diff --git a/src/main/java/de/ph87/data/graph/GraphController.java b/src/main/java/de/ph87/data/graph/GraphController.java new file mode 100644 index 0000000..c042ecf --- /dev/null +++ b/src/main/java/de/ph87/data/graph/GraphController.java @@ -0,0 +1,49 @@ +package de.ph87.data.graph; + +import de.ph87.data.point.Point; +import de.ph87.data.point.PointRequest; +import de.ph87.data.point.PointService; +import de.ph87.data.series.Alignment; +import de.ph87.data.series.Series; +import de.ph87.data.series.SeriesDto; +import de.ph87.data.series.SeriesRepository; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("Series/Graph") +public class GraphController { + + private final SeriesRepository seriesRepository; + + private final PointService pointService; + + @GetMapping(path = "{seriesId}/{width}/{height}/{outerName}/{offset}/{duration}/{innerName}", produces = "image/png") + public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String outerName, @PathVariable final long offset, @PathVariable final long duration, @PathVariable final String innerName) throws IOException { + final Alignment outer = Alignment.valueOf(outerName); + final Alignment inner = Alignment.valueOf(innerName); + final PointRequest request = new PointRequest(outer, offset, duration, inner); + + final Series series = seriesRepository.findById(seriesId).orElseThrow(); + final List points = pointService.getPoints(series, request); + final Graph graph = new Graph(new SeriesDto(series), points, request, width, height, 10); + + final BufferedImage image = graph.draw(); + response.setContentType("image/png"); + ImageIO.write(image, "PNG", response.getOutputStream()); + response.getOutputStream().flush(); + } + +} diff --git a/src/main/java/de/ph87/data/series/graph/GraphPoint.java b/src/main/java/de/ph87/data/point/Point.java similarity index 63% rename from src/main/java/de/ph87/data/series/graph/GraphPoint.java rename to src/main/java/de/ph87/data/point/Point.java index b171a54..b9abd5b 100644 --- a/src/main/java/de/ph87/data/series/graph/GraphPoint.java +++ b/src/main/java/de/ph87/data/point/Point.java @@ -1,22 +1,22 @@ -package de.ph87.data.series.graph; +package de.ph87.data.point; import lombok.*; import java.time.*; @Data -public class GraphPoint { +public class Point { public final ZonedDateTime date; public final double value; @NonNull - public GraphPoint plus(@NonNull final GraphPoint other) { + public Point plus(@NonNull final Point other) { if (this.date.compareTo(other.date) != 0) { throw new RuntimeException("Cannot 'add' GraphPoints with different dates: this=%s, other=%s".formatted(this, other)); } - return new GraphPoint(date, value + other.value); + return new Point(date, value + other.value); } } diff --git a/src/main/java/de/ph87/data/point/PointController.java b/src/main/java/de/ph87/data/point/PointController.java new file mode 100644 index 0000000..8bdc945 --- /dev/null +++ b/src/main/java/de/ph87/data/point/PointController.java @@ -0,0 +1,26 @@ +package de.ph87.data.point; + +import de.ph87.data.view.ViewPointRequest; +import de.ph87.data.view.ViewService; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("points") +public class PointController { + + private final ViewService viewService; + + @PostMapping("fetch") + public List fetch(@RequestBody @NonNull final ViewPointRequest request) { + return viewService.getPoints(request); + } + +} diff --git a/src/main/java/de/ph87/data/point/PointRequest.java b/src/main/java/de/ph87/data/point/PointRequest.java new file mode 100644 index 0000000..f60eb4c --- /dev/null +++ b/src/main/java/de/ph87/data/point/PointRequest.java @@ -0,0 +1,41 @@ +package de.ph87.data.point; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import de.ph87.data.series.Aligned; +import de.ph87.data.series.Alignment; +import lombok.Data; +import lombok.NonNull; + +import java.time.ZonedDateTime; + +@Data +public class PointRequest { + + @NonNull + public final Alignment outer; + + public final long outerOffset; + + public final long outerCount; + + @NonNull + public final Alignment inner; + + @NonNull + @JsonIgnore + public final Aligned begin; + + @NonNull + @JsonIgnore + public final Aligned end; + + public PointRequest(@NonNull final Alignment outer, final long outerOffset, final long outerCount, @NonNull final Alignment inner) { + this.outer = outer; + this.outerOffset = outerOffset; + this.outerCount = outerCount; + this.inner = inner; + this.end = outer.align(ZonedDateTime.now()).plus(1).minus(outerOffset); + this.begin = end.minus(outerCount); + } + +} diff --git a/src/main/java/de/ph87/data/point/PointService.java b/src/main/java/de/ph87/data/point/PointService.java new file mode 100644 index 0000000..c6a34af --- /dev/null +++ b/src/main/java/de/ph87/data/point/PointService.java @@ -0,0 +1,30 @@ +package de.ph87.data.point; + +import de.ph87.data.series.Series; +import de.ph87.data.series.meter.MeterService; +import de.ph87.data.series.varying.VaryingService; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PointService { + + private final VaryingService varyingService; + + private final MeterService meterService; + + @NonNull + public List getPoints(@NonNull final Series series, @NonNull final PointRequest pointRequest) { + return switch (series.getType()) { + case METER -> meterService.getPoints(series, pointRequest); + case VARYING -> varyingService.getPoints(series, pointRequest); + }; + } + +} diff --git a/src/main/java/de/ph87/data/series/SeriesDto.java b/src/main/java/de/ph87/data/series/SeriesDto.java index 6235d55..49af668 100644 --- a/src/main/java/de/ph87/data/series/SeriesDto.java +++ b/src/main/java/de/ph87/data/series/SeriesDto.java @@ -1,5 +1,6 @@ package de.ph87.data.series; +import com.fasterxml.jackson.annotation.JsonIgnore; import de.ph87.data.value.Unit; import de.ph87.data.web.IWebSocketMessage; import jakarta.annotation.Nullable; @@ -14,6 +15,7 @@ import java.util.List; @ToString public class SeriesDto implements IWebSocketMessage { + @JsonIgnore public final List websocketTopic = List.of("Series"); public final long id; diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index 16cf960..de4093e 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -1,14 +1,16 @@ package de.ph87.data.series; -import de.ph87.data.*; -import lombok.*; -import lombok.extern.slf4j.*; -import org.springframework.context.*; -import org.springframework.stereotype.*; -import org.springframework.transaction.annotation.*; +import de.ph87.data.Action; +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.*; -import java.util.function.*; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; @Slf4j @Service @@ -39,11 +41,6 @@ public class SeriesService { return seriesRepository.findById(id).orElseThrow(); } - @NonNull - public SeriesDto getDtoById(final long id) { - return toDto(getById(id)); - } - @NonNull private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) { final SeriesDto dto = toDto(series); diff --git a/src/main/java/de/ph87/data/series/graph/GraphController.java b/src/main/java/de/ph87/data/series/graph/GraphController.java deleted file mode 100644 index 551591a..0000000 --- a/src/main/java/de/ph87/data/series/graph/GraphController.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.ph87.data.series.graph; - -import de.ph87.data.series.Aligned; -import de.ph87.data.series.Alignment; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.time.ZonedDateTime; - -@Slf4j -@RestController -@RequiredArgsConstructor -@RequestMapping("Series/Graph") -public class GraphController { - - private final GraphService graphService; - - @GetMapping(path = "{seriesId}/{width}/{height}/{outerAlignmentName}/{offset}/{duration}/{innerAlignmentName}", produces = "image/png") - public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String outerAlignmentName, @PathVariable final long offset, @PathVariable final long duration, @PathVariable final String innerAlignmentName) throws IOException { - final Alignment outerAlignment = Alignment.valueOf(outerAlignmentName); - final Alignment innerAlignment = Alignment.valueOf(innerAlignmentName); - final Aligned end = outerAlignment.align(ZonedDateTime.now()).plus(1).minus(offset); - final Aligned begin = end.minus(duration); - log.warn("Graph: outer={}, offset={}, duration={}, begin={}, end={}, inner={}", outerAlignment, offset, duration, begin.date.toLocalDateTime(), end.date.toLocalDateTime(), innerAlignment); - final Graph graph = graphService.getGraph(seriesId, innerAlignment, begin, end, width, height, 10); - final BufferedImage image = graph.draw(); - response.setContentType("image/png"); - ImageIO.write(image, "PNG", response.getOutputStream()); - response.getOutputStream().flush(); - } - -} diff --git a/src/main/java/de/ph87/data/series/graph/GraphService.java b/src/main/java/de/ph87/data/series/graph/GraphService.java deleted file mode 100644 index 3d9c202..0000000 --- a/src/main/java/de/ph87/data/series/graph/GraphService.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.ph87.data.series.graph; - -import de.ph87.data.series.Aligned; -import de.ph87.data.series.Alignment; -import de.ph87.data.series.SeriesDto; -import de.ph87.data.series.SeriesService; -import de.ph87.data.series.meter.MeterService; -import de.ph87.data.series.varying.VaryingService; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class GraphService { - - private final SeriesService seriesService; - - private final VaryingService varyingService; - - private final MeterService meterService; - - @NonNull - public Graph getGraph(final long seriesId, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) { - final SeriesDto series = seriesService.getDtoById(seriesId); - final List entries = switch (series.getType()) { - case METER -> meterService.getPoints(series, innerAlignment, begin, end); - case VARYING -> varyingService.getPoints(series, innerAlignment, begin, end); - }; - return new Graph(series, entries, innerAlignment, begin, end, width, height, border); - } - -} diff --git a/src/main/java/de/ph87/data/series/meter/MeterService.java b/src/main/java/de/ph87/data/series/meter/MeterService.java index 4d02fdb..88015e4 100644 --- a/src/main/java/de/ph87/data/series/meter/MeterService.java +++ b/src/main/java/de/ph87/data/series/meter/MeterService.java @@ -1,22 +1,31 @@ package de.ph87.data.series.meter; +import de.ph87.data.point.Point; +import de.ph87.data.point.PointRequest; import de.ph87.data.series.*; -import de.ph87.data.series.graph.*; -import de.ph87.data.series.meter.day.*; -import de.ph87.data.series.meter.five.*; -import de.ph87.data.series.meter.hour.*; -import de.ph87.data.series.meter.month.*; -import de.ph87.data.series.meter.week.*; -import de.ph87.data.series.meter.year.*; -import lombok.*; -import lombok.extern.slf4j.*; +import de.ph87.data.series.meter.day.MeterDay; +import de.ph87.data.series.meter.day.MeterDayRepository; +import de.ph87.data.series.meter.five.MeterFive; +import de.ph87.data.series.meter.five.MeterFiveRepository; +import de.ph87.data.series.meter.hour.MeterHour; +import de.ph87.data.series.meter.hour.MeterHourRepository; +import de.ph87.data.series.meter.month.MeterMonth; +import de.ph87.data.series.meter.month.MeterMonthRepository; +import de.ph87.data.series.meter.week.MeterWeek; +import de.ph87.data.series.meter.week.MeterWeekRepository; +import de.ph87.data.series.meter.year.MeterYear; +import de.ph87.data.series.meter.year.MeterYearRepository; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; -import org.springframework.stereotype.*; -import org.springframework.transaction.annotation.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.*; -import java.util.function.*; -import java.util.stream.*; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; @Slf4j @Service @@ -80,13 +89,13 @@ public class MeterService { } @NonNull - public List getPoints(@NonNull final SeriesDto series, @NonNull final Alignment alignment, @NonNull final Aligned begin, @NonNull final Aligned end) { - final List graphPoints = findRepository(alignment).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.id, begin.date, end.date); - final List points = graphPoints.stream().map(meterValue -> new GraphPoint(meterValue.getId().getDate(), meterValue.getMax() - meterValue.getMin())).collect(Collectors.toCollection(LinkedList::new)); + public List getPoints(final @NonNull Series series, @NonNull final PointRequest pointRequest) { + final List graphPoints = findRepository(pointRequest.inner).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.getId(), pointRequest.begin.date, pointRequest.end.date); + final List points = graphPoints.stream().map(meterValue -> new Point(meterValue.getId().getDate(), meterValue.getMax() - meterValue.getMin())).collect(Collectors.toCollection(LinkedList::new)); for (int i = 0; i < points.size() - 1; i++) { if (points.get(i).date.compareTo(points.get(i + 1).date) == 0) { - final GraphPoint first = points.remove(i); - final GraphPoint second = points.remove(i + 1); + final Point first = points.remove(i); + final Point second = points.remove(i + 1); points.add(i, first.plus(second)); } } diff --git a/src/main/java/de/ph87/data/series/varying/VaryingService.java b/src/main/java/de/ph87/data/series/varying/VaryingService.java index 6a355dc..e2de9df 100644 --- a/src/main/java/de/ph87/data/series/varying/VaryingService.java +++ b/src/main/java/de/ph87/data/series/varying/VaryingService.java @@ -1,7 +1,8 @@ package de.ph87.data.series.varying; +import de.ph87.data.point.Point; +import de.ph87.data.point.PointRequest; import de.ph87.data.series.*; -import de.ph87.data.series.graph.GraphPoint; import de.ph87.data.series.varying.day.VaryingDay; import de.ph87.data.series.varying.day.VaryingDayRepository; import de.ph87.data.series.varying.five.VaryingFive; @@ -72,12 +73,11 @@ public class VaryingService { } @NonNull - public List getPoints(@NonNull final SeriesDto series, @NonNull final Alignment innerAlignment, @NonNull final Aligned begin, @NonNull final Aligned end) { - log.info("getPoints: innerAlignment={}, begin={}, end={}", innerAlignment, begin, end); - return findRepository(innerAlignment) - .findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.id, begin.date, end.date) + public List getPoints(final @NonNull Series series, @NonNull final PointRequest pointRequest) { + return findRepository(pointRequest.inner) + .findAllByIdSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqual(series.getId(), pointRequest.begin.date, pointRequest.end.date) .stream() - .map(v -> new GraphPoint(v.getId().getDate(), v.getAvg())) + .map(v -> new Point(v.getId().getDate(), v.getAvg())) .toList(); } diff --git a/src/main/java/de/ph87/data/view/ViewController.java b/src/main/java/de/ph87/data/view/ViewController.java new file mode 100644 index 0000000..7ddfced --- /dev/null +++ b/src/main/java/de/ph87/data/view/ViewController.java @@ -0,0 +1,23 @@ +package de.ph87.data.view; + +import de.ph87.data.view.tree.ViewDto; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("View") +public class ViewController { + + private final ViewRepository viewRepository; + + @GetMapping("rootList") + public List rootList() { + return viewRepository.findAllDtoByNameNotEmpty(); + } + +} diff --git a/src/main/java/de/ph87/data/view/ViewPointRequest.java b/src/main/java/de/ph87/data/view/ViewPointRequest.java new file mode 100644 index 0000000..d713bbc --- /dev/null +++ b/src/main/java/de/ph87/data/view/ViewPointRequest.java @@ -0,0 +1,20 @@ +package de.ph87.data.view; + +import de.ph87.data.point.PointRequest; +import de.ph87.data.series.Alignment; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +public class ViewPointRequest extends PointRequest { + + public final String viewUuid; + + public ViewPointRequest(final @NonNull Alignment outer, final int outerOffset, final int outerCount, final @NonNull Alignment inner, @NonNull final String viewUuid) { + super(outer, outerOffset, outerCount, inner); + this.viewUuid = viewUuid; + } + +} diff --git a/src/main/java/de/ph87/data/view/ViewRepository.java b/src/main/java/de/ph87/data/view/ViewRepository.java new file mode 100644 index 0000000..1e0b94b --- /dev/null +++ b/src/main/java/de/ph87/data/view/ViewRepository.java @@ -0,0 +1,20 @@ +package de.ph87.data.view; + +import de.ph87.data.view.tree.View; +import de.ph87.data.view.tree.ViewDto; +import lombok.NonNull; +import org.springframework.data.repository.ListCrudRepository; + +import java.util.List; + +public interface ViewRepository extends ListCrudRepository { + + boolean existsByName(@NonNull String name); + + List findAllByNameNot(@NonNull String name); + + default List findAllDtoByNameNotEmpty() { + return findAllByNameNot("").stream().map(ViewDto::map).toList(); + } + +} diff --git a/src/main/java/de/ph87/data/view/ViewScope.java b/src/main/java/de/ph87/data/view/ViewScope.java new file mode 100644 index 0000000..df3f120 --- /dev/null +++ b/src/main/java/de/ph87/data/view/ViewScope.java @@ -0,0 +1,46 @@ +package de.ph87.data.view; + +import de.ph87.data.point.Point; +import de.ph87.data.point.PointRequest; +import de.ph87.data.point.PointService; +import de.ph87.data.series.Series; +import lombok.Data; +import lombok.NonNull; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +public class ViewScope { + + private final PointService pointService; + + private final PointRequest request; + + private final Map> seriesPoints = new HashMap<>(); + + private final Map> literalPoints = new HashMap<>(); + + @NonNull + public List getPoints(@NonNull final Series series) { + return seriesPoints.computeIfAbsent(series.getId(), ignore -> pointService.getPoints(series, request)); + } + + @NonNull + public List getLiteral(final double value) { + return literalPoints.computeIfAbsent(value, this::_generateLiteral); + } + + @NonNull + private List _generateLiteral(final double value) { + final List points = new ArrayList<>(); + for (ZonedDateTime date = request.begin.date; !date.isAfter(request.end.date); date = request.inner.plus(date, 1)) { + points.add(new Point(date, value)); + } + return points; + } + +} diff --git a/src/main/java/de/ph87/data/view/ViewService.java b/src/main/java/de/ph87/data/view/ViewService.java new file mode 100644 index 0000000..b5f8010 --- /dev/null +++ b/src/main/java/de/ph87/data/view/ViewService.java @@ -0,0 +1,58 @@ +package de.ph87.data.view; + +import de.ph87.data.point.Point; +import de.ph87.data.point.PointService; +import de.ph87.data.series.SeriesRepository; +import de.ph87.data.view.tree.View; +import de.ph87.data.view.tree.ViewBinary; +import de.ph87.data.view.tree.ViewSeries; +import de.ph87.data.view.tree.ViewUnary; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ViewService { + + private static final long DEMO_SERIES_POWER_BALANCE = 8L; + + private static final long DEMO_SERIES_POWER_PRODUCED = 26L; + + private static final String DEMO_POWER_CONSUMED = "__DEMO_POWER_CONSUMED__"; + + private final ViewRepository viewRepository; + + private final PointService pointService; + + private final SeriesRepository seriesRepository; + + @Transactional + @EventListener(ApplicationReadyEvent.class) + public void init() { + if (!viewRepository.existsByName(DEMO_POWER_CONSUMED)) { + final View powerBalance = new ViewSeries(seriesRepository.findById(DEMO_SERIES_POWER_BALANCE).orElseThrow()); + final View powerBalance2 = new ViewBinary(ViewBinary.Operation.PLUS, powerBalance, powerBalance); + final View powerPurchased = new ViewUnary(ViewUnary.Operation.NOT_NEG, powerBalance2); + final View powerProduced = new ViewSeries(seriesRepository.findById(DEMO_SERIES_POWER_PRODUCED).orElseThrow()); + final View powerConsumed = viewRepository.save(new ViewBinary(ViewBinary.Operation.PLUS, powerPurchased, powerProduced)); + powerConsumed.setName(DEMO_POWER_CONSUMED); + log.warn("DEMO VIEW CREATED: {} \"{}\"", powerConsumed.getUuid(), powerConsumed.getName()); + } + } + + @Transactional(readOnly = true) + public List getPoints(@NonNull final ViewPointRequest request) { + final ViewScope scope = new ViewScope(pointService, request); + final View view = viewRepository.findById(request.viewUuid).orElseThrow(); + return view.getPoints(scope); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/View.java b/src/main/java/de/ph87/data/view/tree/View.java new file mode 100644 index 0000000..24bb945 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/View.java @@ -0,0 +1,37 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.point.Point; +import de.ph87.data.view.ViewScope; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; +import java.util.UUID; + +@Entity +@Getter +@ToString +@DiscriminatorColumn(name = "_type_") +@Inheritance(strategy = InheritanceType.JOINED) +public abstract class View { + + @Id + private String uuid = UUID.randomUUID().toString(); + + @Version + private int version; + + @Column(insertable = false, updatable = false) + private String _type_; + + @Setter + @NonNull + @Column(nullable = false) + private String name = ""; + + public abstract List getPoints(final @NonNull ViewScope scope); + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewBinary.java b/src/main/java/de/ph87/data/view/tree/ViewBinary.java new file mode 100644 index 0000000..681e3cc --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewBinary.java @@ -0,0 +1,84 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.point.Point; +import de.ph87.data.view.ViewScope; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +@DiscriminatorValue("binary") +public class ViewBinary extends View { + + @NonNull + @Enumerated(EnumType.STRING) + private Operation operation; + + @NonNull + @OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL) + private View view0; + + @NonNull + @OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL) + private View view1; + + public ViewBinary(@NonNull final Operation operation, @NonNull final View view0, @NonNull final View view1) { + this.operation = operation; + this.view0 = view0; + this.view1 = view1; + } + + public enum Operation { + PLUS(Double::sum), + MINUS((a, b) -> a - b), + MULTIPLY((a, b) -> a * b), + DIVIDE((a, b) -> a / b), + MODULO((a, b) -> a % b), + PERCENT((a, b) -> a / b * 100), + ; + + public final BiFunction function; + + Operation(final BiFunction function) { + this.function = function; + } + + @NonNull + public Point apply(@NonNull final Point a, @NonNull final Point b) { + return new Point(a.date, function.apply(a.value, b.value)); + } + + } + + @Override + public List getPoints(final @NonNull ViewScope scope) { + final List pointsA = view0.getPoints(scope); + final List pointsB = view1.getPoints(scope); + final List result = new ArrayList<>(pointsA.size() + pointsB.size()); + int indexA = 0; + int indexB = 0; + while (indexA < pointsA.size() && indexB < pointsB.size()) { + final Point pointA = pointsA.get(indexA); + final Point pointB = pointsB.get(indexB); + int cmp = pointA.date.compareTo(pointB.date); + if (cmp < 0) { + indexA++; + } else if (cmp > 0) { + indexB++; + } else { + result.add(operation.apply(pointA, pointB)); + indexA++; + indexB++; + } + } + return result; + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java b/src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java new file mode 100644 index 0000000..fb4dfe8 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewBinaryDto.java @@ -0,0 +1,27 @@ +package de.ph87.data.view.tree; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +public class ViewBinaryDto extends ViewDto { + + @NonNull + public final ViewBinary.Operation operation; + + @NonNull + public final ViewDto view0; + + @NonNull + public final ViewDto view1; + + public ViewBinaryDto(@NonNull final ViewBinary viewBinary) { + super(viewBinary); + this.operation = viewBinary.getOperation(); + this.view0 = ViewDto.map(viewBinary.getView0()); + this.view1 = ViewDto.map(viewBinary.getView1()); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewDto.java b/src/main/java/de/ph87/data/view/tree/ViewDto.java new file mode 100644 index 0000000..adec0aa --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewDto.java @@ -0,0 +1,34 @@ +package de.ph87.data.view.tree; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +public abstract class ViewDto { + + public final String _type_; + + public final String uuid; + + public final String name; + + protected ViewDto(@NonNull final View view) { + this._type_ = view.get_type_(); + this.uuid = view.getUuid(); + this.name = view.getName(); + } + + @NonNull + public static ViewDto map(@NonNull final View view) { + return switch (view) { + case final ViewLiteral viewLiteral -> new ViewLiteralDto(viewLiteral); + case final ViewSeries viewSeries -> new ViewSeriesDto(viewSeries); + case final ViewUnary viewUnary -> new ViewUnaryDto(viewUnary); + case final ViewBinary viewBinary -> new ViewBinaryDto(viewBinary); + default -> throw new RuntimeException("DTO mapping of View type not implemented: " + view.getClass()); + }; + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewLiteral.java b/src/main/java/de/ph87/data/view/tree/ViewLiteral.java new file mode 100644 index 0000000..5c4c5a6 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewLiteral.java @@ -0,0 +1,32 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.point.Point; +import de.ph87.data.view.ViewScope; +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.*; + +import java.util.List; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +@DiscriminatorValue("literal") +public class ViewLiteral extends View { + + @Column(name = "`value`", nullable = false) + private double value; + + public ViewLiteral(final double value) { + this.value = value; + } + + @Override + public List getPoints(final @NonNull ViewScope scope) { + return scope.getLiteral(value); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java b/src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java new file mode 100644 index 0000000..5a8b390 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewLiteralDto.java @@ -0,0 +1,18 @@ +package de.ph87.data.view.tree; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +public class ViewLiteralDto extends ViewDto { + + public final double value; + + public ViewLiteralDto(@NonNull final ViewLiteral viewLiteral) { + super(viewLiteral); + this.value = viewLiteral.getValue(); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewSeries.java b/src/main/java/de/ph87/data/view/tree/ViewSeries.java new file mode 100644 index 0000000..4881bfc --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewSeries.java @@ -0,0 +1,34 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.point.Point; +import de.ph87.data.series.Series; +import de.ph87.data.view.ViewScope; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.*; + +import java.util.List; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +@DiscriminatorValue("series") +public class ViewSeries extends View { + + @NonNull + @ManyToOne(optional = false) + private Series series; + + public ViewSeries(@NonNull final Series series) { + this.series = series; + } + + @Override + public List getPoints(final @NonNull ViewScope scope) { + return scope.getPoints(series); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java b/src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java new file mode 100644 index 0000000..33c4d64 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewSeriesDto.java @@ -0,0 +1,20 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.series.SeriesDto; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +public class ViewSeriesDto extends ViewDto { + + @NonNull + public final SeriesDto series; + + public ViewSeriesDto(@NonNull final ViewSeries viewSeries) { + super(viewSeries); + this.series = new SeriesDto(viewSeries.getSeries()); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewUnary.java b/src/main/java/de/ph87/data/view/tree/ViewUnary.java new file mode 100644 index 0000000..335bc0b --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewUnary.java @@ -0,0 +1,56 @@ +package de.ph87.data.view.tree; + +import de.ph87.data.point.Point; +import de.ph87.data.view.ViewScope; +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.function.Function; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +@DiscriminatorValue("unary") +public class ViewUnary extends View { + + @NonNull + @Enumerated(EnumType.STRING) + private Operation operation; + + @NonNull + @OneToOne(optional = false, orphanRemoval = true, cascade = CascadeType.ALL) + private View view; + + public ViewUnary(@NonNull final Operation operation, @NonNull final View view) { + this.operation = operation; + this.view = view; + } + + public enum Operation { + NEG(a -> -a), + REC(a -> 1 / a), + NOT_NEG(a -> a < 0 ? 0 : a), + ; + + public final Function function; + + Operation(final Function function) { + this.function = function; + } + + @NonNull + public Point apply(@NonNull final Point point) { + return new Point(point.date, function.apply(point.value)); + } + + } + + @Override + public List getPoints(final @NonNull ViewScope scope) { + return view.getPoints(scope).stream().map(operation::apply).toList(); + } + +} diff --git a/src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java b/src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java new file mode 100644 index 0000000..683d693 --- /dev/null +++ b/src/main/java/de/ph87/data/view/tree/ViewUnaryDto.java @@ -0,0 +1,23 @@ +package de.ph87.data.view.tree; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +public class ViewUnaryDto extends ViewDto { + + @NonNull + public final ViewUnary.Operation operation; + + @NonNull + public final ViewDto view; + + public ViewUnaryDto(@NonNull final ViewUnary viewUnary) { + super(viewUnary); + this.operation = viewUnary.getOperation(); + this.view = ViewDto.map(viewUnary.getView()); + } + +} diff --git a/src/main/java/de/ph87/data/weather/WeatherService.java b/src/main/java/de/ph87/data/weather/WeatherService.java index aba6c22..22f1b0a 100644 --- a/src/main/java/de/ph87/data/weather/WeatherService.java +++ b/src/main/java/de/ph87/data/weather/WeatherService.java @@ -61,7 +61,7 @@ public class WeatherService { days = newDays; log.info("Weather update complete"); } catch (Exception e) { - log.error("Failed fetching Weather data!", e); + log.error("Failed fetching Weather data: {}", e.toString()); } }