From d65628a7d6683019aa35c131d1ddcd667b2c1a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Mon, 27 Oct 2025 08:39:57 +0100 Subject: [PATCH] GraphOperation --- src/main/angular/src/app/COMMON.ts | 8 ++ .../angular/src/app/plot/axis/graph/Graph.ts | 22 ++++- .../plot/editor/plot-editor.component.html | 26 ++++- .../app/plot/editor/plot-editor.component.ts | 15 ++- src/main/angular/src/app/plot/plot.service.ts | 22 ++++- .../src/app/plot/plot/plot.component.ts | 22 ++--- src/main/angular/src/app/series/MinMaxAvg.ts | 21 ++--- .../angular/src/app/series/series.service.ts | 15 ++- src/main/java/de/ph87/data/DemoService.java | 94 ++++++++++++++----- src/main/java/de/ph87/data/Helpers.java | 9 ++ .../de/ph87/data/plot/axis/graph/Graph.java | 23 ++++- .../data/plot/axis/graph/GraphController.java | 25 ++++- .../plot/axis/graph/GraphDivisionByZero.java | 5 + .../ph87/data/plot/axis/graph/GraphDto.java | 19 +++- .../data/plot/axis/graph/GraphOperation.java | 27 ++++++ .../axis/graph/GraphOperationFunction.java | 8 ++ .../data/series/AllSeriesPointResponse.java | 2 +- .../data/series/OneSeriesPointsRequest.java | 19 ++++ .../data/series/OneSeriesPointsResponse.java | 2 +- .../java/de/ph87/data/series/SeriesPoint.java | 58 +++++++++++- .../de/ph87/data/series/SeriesService.java | 23 +++-- .../ph87/data/series/data/bool/BoolPoint.java | 34 ++++++- .../data/series/data/delta/DeltaPoint.java | 22 ++++- .../data/series/data/delta/DeltaRepo.java | 2 +- .../series/data/varying/VaryingPoint.java | 32 ++++++- 25 files changed, 454 insertions(+), 101 deletions(-) create mode 100644 src/main/java/de/ph87/data/plot/axis/graph/GraphDivisionByZero.java create mode 100644 src/main/java/de/ph87/data/plot/axis/graph/GraphOperation.java create mode 100644 src/main/java/de/ph87/data/plot/axis/graph/GraphOperationFunction.java diff --git a/src/main/angular/src/app/COMMON.ts b/src/main/angular/src/app/COMMON.ts index 5aa3ddc..a076f76 100644 --- a/src/main/angular/src/app/COMMON.ts +++ b/src/main/angular/src/app/COMMON.ts @@ -53,6 +53,14 @@ export function validateBoolean(json: any): boolean { return json; } +export function validateEnum>(value: any, enumType: T): T[keyof T] { + const str = validateString(value); + if (Object.values(enumType).includes(str)) { + return str as T[keyof T]; + } + throw new Error(`Invalid enum value: ${str}`); +} + export function validateList(json: any, fromJson: FromJson): T[] { return json.map(fromJson); } diff --git a/src/main/angular/src/app/plot/axis/graph/Graph.ts b/src/main/angular/src/app/plot/axis/graph/Graph.ts index aa5fd53..386eb95 100644 --- a/src/main/angular/src/app/plot/axis/graph/Graph.ts +++ b/src/main/angular/src/app/plot/axis/graph/Graph.ts @@ -1,10 +1,20 @@ import {Series} from "../../../series/Series"; import {Group} from "../../Group"; -import {validateBoolean, validateNumber, validateString} from "../../../COMMON"; +import {mapNotNull, validateBoolean, validateEnum, validateNumber, validateString} from "../../../COMMON"; import {Axis} from '../Axis'; import {SeriesType} from '../../../series/SeriesType'; import {GraphType} from './GraphType'; +export enum GraphOperation { + MINUS = 'MINUS', + PLUS = 'PLUS', + DIVIDE = 'DIVIDE', +} + +export function listGraphOperation() { + return Object.keys(GraphOperation); +} + export class Graph { readonly showMin: boolean; @@ -18,12 +28,15 @@ export class Graph { readonly id: number, readonly version: number, readonly series: Series, + readonly factor: number, + readonly operation: GraphOperation, + readonly series2: Series | null, + readonly factor2: number, readonly name: string, readonly visible: boolean, readonly type: GraphType, readonly fill: string, readonly color: string, - readonly factor: number, readonly group: Group, readonly stack: string, readonly min: boolean, @@ -41,12 +54,15 @@ export class Graph { validateNumber(json.id), validateNumber(json.version), Series.fromJson(json.series), + validateNumber(json.factor), + validateEnum(json.operation, GraphOperation), + mapNotNull(json.series2, Series.fromJson), + validateNumber(json.factor2), validateString(json.name), validateBoolean(json.visible), GraphType.fromJson(json.type), validateString(json.fill), validateString(json.color), - validateNumber(json.factor), validateString(json.group) as Group, validateString(json.stack), validateBoolean(json.min), diff --git a/src/main/angular/src/app/plot/editor/plot-editor.component.html b/src/main/angular/src/app/plot/editor/plot-editor.component.html index 5bb63f8..b52d122 100644 --- a/src/main/angular/src/app/plot/editor/plot-editor.component.html +++ b/src/main/angular/src/app/plot/editor/plot-editor.component.html @@ -136,9 +136,9 @@ Name Serie + Faktor Typ Farbe - Faktor Aggregat Stack min @@ -163,6 +163,27 @@ } + + + + + + + + + + + + @for (group of groups(); track group) { diff --git a/src/main/angular/src/app/plot/editor/plot-editor.component.ts b/src/main/angular/src/app/plot/editor/plot-editor.component.ts index 0e7f3a5..27b78ca 100644 --- a/src/main/angular/src/app/plot/editor/plot-editor.component.ts +++ b/src/main/angular/src/app/plot/editor/plot-editor.component.ts @@ -19,6 +19,7 @@ import {Location} from '@angular/common'; import {PlotComponent} from '../plot/plot.component'; import {ActivatedRoute} from '@angular/router'; import {GraphType} from '../axis/graph/GraphType'; +import {GraphOperation, listGraphOperation} from '../axis/graph/Graph'; Chart.register( CategoryScale, @@ -35,13 +36,6 @@ Chart.register( Filler, ); -export function unitInBrackets(unit: string): string { - if (!unit) { - return ''; - } - return ` [${unit}]`; -} - @Component({ selector: 'app-plot-editor', imports: [ @@ -58,6 +52,12 @@ export function unitInBrackets(unit: string): string { }) export class PlotEditor implements OnInit, OnDestroy { + protected readonly GraphType = GraphType; + + protected readonly GraphOperation = GraphOperation; + + protected readonly listGraphOperation = listGraphOperation; + protected readonly SeriesType = SeriesType; protected readonly Interval = Interval; @@ -142,5 +142,4 @@ export class PlotEditor implements OnInit, OnDestroy { return Object.keys(Group); } - protected readonly GraphType = GraphType; } diff --git a/src/main/angular/src/app/plot/plot.service.ts b/src/main/angular/src/app/plot/plot.service.ts index 675e34d..e6410b0 100644 --- a/src/main/angular/src/app/plot/plot.service.ts +++ b/src/main/angular/src/app/plot/plot.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {ApiService, EntityListService, Next} from '../COMMON'; import {Plot} from './Plot'; import {Axis} from './axis/Axis'; -import {Graph} from './axis/graph/Graph'; +import {Graph, GraphOperation} from './axis/graph/Graph'; import {Group} from './Group'; import {Interval} from '../series/Interval'; import {GraphType} from './axis/graph/GraphType'; @@ -118,14 +118,26 @@ export class PlotService extends EntityListService { this.postSingle(['Graph', graph.id, 'series'], value, next); } - graphColor(graph: Graph, value: string, next?: Next): void { - this.postSingle(['Graph', graph.id, 'color'], value, next); - } - graphFactor(graph: Graph, value: number, next?: Next): void { this.postSingle(['Graph', graph.id, 'factor'], value, next); } + graphOperation(graph: Graph, value: GraphOperation, next?: Next): void { + this.postSingle(['Graph', graph.id, 'operation'], value, next); + } + + graphSeries2(graph: Graph, value: number, next?: Next): void { + this.postSingle(['Graph', graph.id, 'series2'], value, next); + } + + graphFactor2(graph: Graph, value: number, next?: Next): void { + this.postSingle(['Graph', graph.id, 'factor2'], value, next); + } + + graphColor(graph: Graph, value: string, next?: Next): void { + this.postSingle(['Graph', graph.id, 'color'], value, next); + } + graphGroup(graph: Graph, value: Group, next?: Next): void { this.postSingle(['Graph', graph.id, 'group'], value, next); } diff --git a/src/main/angular/src/app/plot/plot/plot.component.ts b/src/main/angular/src/app/plot/plot/plot.component.ts index 1f4f262..fb00123 100644 --- a/src/main/angular/src/app/plot/plot/plot.component.ts +++ b/src/main/angular/src/app/plot/plot/plot.component.ts @@ -216,24 +216,21 @@ export class PlotComponent implements AfterViewInit, OnDestroy { private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void { this.seriesService.oneSeriesPoints( - graph.series, - graph.axis.plot.interval, - graph.axis.plot.offset, - graph.axis.plot.duration, + graph, points => { if (graph.series.type === SeriesType.BOOL) { - mid.data = toBool(points, graph.factor); + mid.data = toBool(points); } else if (graph.series.type === SeriesType.DELTA) { - mid.data = toDelta(points, graph.factor); + mid.data = toDelta(points); } else if (graph.series.type === SeriesType.VARYING) { if (min) { - min.data = toMin(points, graph.factor); + min.data = toMin(points); } if (max) { - max.data = toMax(points, graph.factor); + max.data = toMax(points); } if (mid) { - mid.data = toAvg(points, graph.factor); + mid.data = toAvg(points); } } this.chart.update(); @@ -264,14 +261,13 @@ export class PlotComponent implements AfterViewInit, OnDestroy { private updatePoint(graph: Graph, dataset: Dataset, date: Date, y: number): void { const x = date.getTime(); const point = dataset.data.filter((p: PointElement) => p.x === x)[0]; - const yMultiplied = y * graph.factor; if (point) { - if (point.y !== yMultiplied) { - point.y = yMultiplied; + if (point.y !== y) { + point.y = y * graph.factor; // TODO this is wrong. we need to take GraphOperation into account this.chart.update(); } } else { - dataset.data.push({x: x, y: yMultiplied}); // TODO check if this is a LIVE/SCROLLING plot (right end of plot is 'now') + 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(); } } diff --git a/src/main/angular/src/app/series/MinMaxAvg.ts b/src/main/angular/src/app/series/MinMaxAvg.ts index 068c2b3..3bed60c 100644 --- a/src/main/angular/src/app/series/MinMaxAvg.ts +++ b/src/main/angular/src/app/series/MinMaxAvg.ts @@ -9,10 +9,7 @@ export class Point { } -export type PointMapper = (p: number[][], factor: number) => Point[]; - -// noinspection JSUnusedLocalSymbols -export function toBool(points: number[][], factor: number): Point[] { +export function toBool(points: number[][]): Point[] { const result = []; let postPone: Point | null = null; for (const p of points) { @@ -44,45 +41,45 @@ export function toBool(points: number[][], factor: number): Point[] { return result; } -export function toDelta(points: number[][], factor: number): Point[] { +export function toDelta(points: number[][]): Point[] { const result = []; for (const p of points) { result.push({ x: p[0] * 1000, - y: (p[1]) * factor, + y: (p[1]), }); } return result; } -export function toMin(points: number[][], factor: number): Point[] { +export function toMin(points: number[][]): Point[] { const result = []; for (const p of points) { result.push({ x: p[0] * 1000, - y: p[1] * factor, + y: p[1], }); } return result; } -export function toMax(points: number[][], factor: number): Point[] { +export function toMax(points: number[][]): Point[] { const result = []; for (const p of points) { result.push({ x: p[0] * 1000, - y: p[2] * factor, + y: p[2], }); } return result; } -export function toAvg(points: number[][], factor: number): Point[] { +export function toAvg(points: number[][]): Point[] { const result = []; for (const p of points) { result.push({ x: p[0] * 1000, - y: p[3] * factor, + y: p[3], }); } return result; diff --git a/src/main/angular/src/app/series/series.service.ts b/src/main/angular/src/app/series/series.service.ts index fe9bf54..e65ce36 100644 --- a/src/main/angular/src/app/series/series.service.ts +++ b/src/main/angular/src/app/series/series.service.ts @@ -4,6 +4,7 @@ import {Series} from './Series'; import {Interval} from './Interval'; import {AllSeriesPointResponse} from './AllSeriesPointResponse'; import {AllSeriesPointRequest} from './AllSeriesPointRequest'; +import {Graph} from "../plot/axis/graph/Graph"; @Injectable({ providedIn: 'root', @@ -16,12 +17,16 @@ export class SeriesService extends EntityListService { super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName); } - oneSeriesPoints(series: Series, interval: Interval, offset: number, duration: number, next: Next): void { + oneSeriesPoints(graph: Graph, next: Next): void { const request = { - id: series.id, - interval: interval.name, - offset: offset, - duration: duration, + id: graph.series.id, + factor: graph.factor, + operation: graph.operation, + id2: graph.series2?.id, + factor2: graph.factor2, + interval: graph.axis.plot.interval.name, + offset: graph.axis.plot.offset, + duration: graph.axis.plot.duration, }; this.api.postList([...this.path, 'oneSeriesPoints'], request, (outer: any[]) => outer.map(validateNumber), next); } diff --git a/src/main/java/de/ph87/data/DemoService.java b/src/main/java/de/ph87/data/DemoService.java index c813417..6a8f7d3 100644 --- a/src/main/java/de/ph87/data/DemoService.java +++ b/src/main/java/de/ph87/data/DemoService.java @@ -43,7 +43,6 @@ public class DemoService { @EventListener(ApplicationReadyEvent.class) public void init() { topics(); -// plots(); } private void topics() { @@ -168,6 +167,7 @@ public class DemoService { zuhauseEnergie(); zuhauseTemperatur(); eltern(); + leistungVergleich(); } private void zuhauseEnergie() { @@ -181,30 +181,33 @@ public class DemoService { 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.setType(GraphType.BAR); - electricityEnergyPurchaseGraph.setStack("a"); - electricityEnergyPurchaseGraph.setName("Bezug"); - electricityEnergyPurchaseGraph.setColor("#FF8800"); - energy.addGraph(electricityEnergyPurchaseGraph); + final String stack = "a"; final Series electricityEnergyDelivery = seriesRepository.findByName("electricity/energy/delivery").orElseThrow(); final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); electricityEnergyDeliveryGraph.setType(GraphType.BAR); - electricityEnergyDeliveryGraph.setStack("a"); - electricityEnergyDeliveryGraph.setName("Überschuss"); + electricityEnergyDeliveryGraph.setStack(stack); + electricityEnergyDeliveryGraph.setName("Zuhause Überschuss"); electricityEnergyDeliveryGraph.setColor("#FF00FF"); electricityEnergyDeliveryGraph.setFactor(-1); energy.addGraph(electricityEnergyDeliveryGraph); final Series electricityEnergyProduce = seriesRepository.findByName("electricity/energy/produce").orElseThrow(); final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); + electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery); + electricityEnergyProduceGraph.setName("Zuhause Eigenverbrauch"); electricityEnergyProduceGraph.setType(GraphType.BAR); - electricityEnergyProduceGraph.setStack("a"); - electricityEnergyProduceGraph.setName("Produktion"); - electricityEnergyProduceGraph.setColor("#0000FF"); + electricityEnergyProduceGraph.setStack(stack); + electricityEnergyProduceGraph.setColor("#008800"); energy.addGraph(electricityEnergyProduceGraph); + + final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow(); + final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); + electricityEnergyPurchaseGraph.setType(GraphType.BAR); + electricityEnergyPurchaseGraph.setStack(stack); + electricityEnergyPurchaseGraph.setName("Zuhause Bezug"); + electricityEnergyPurchaseGraph.setColor("#FF8800"); + energy.addGraph(electricityEnergyPurchaseGraph); } private void zuhauseTemperatur() { @@ -236,6 +239,24 @@ public class DemoService { gardenTemperatureGraph.setAvg(false); gardenTemperatureGraph.setMax(true); temperature.addGraph(gardenTemperatureGraph); + + final Series bufferTemperature = seriesRepository.findByName("heating/buffer/inside/temperature").orElseThrow(); + final Graph bufferTemperatureGraph = graphRepository.save(new Graph(temperature, bufferTemperature)); + bufferTemperatureGraph.setName("Puffer"); + bufferTemperatureGraph.setColor("#FF00FF"); + bufferTemperatureGraph.setMin(true); + bufferTemperatureGraph.setAvg(false); + bufferTemperatureGraph.setMax(true); + temperature.addGraph(bufferTemperatureGraph); + + final Series circuitTemperature = seriesRepository.findByName("heating/circuit/supply/temperature").orElseThrow(); + final Graph circuitTemperatureGraph = graphRepository.save(new Graph(temperature, circuitTemperature)); + circuitTemperatureGraph.setName("Heizkreis"); + circuitTemperatureGraph.setColor("#FF0000"); + circuitTemperatureGraph.setMin(true); + circuitTemperatureGraph.setAvg(false); + circuitTemperatureGraph.setMax(true); + temperature.addGraph(circuitTemperatureGraph); } private void eltern() { @@ -249,14 +270,6 @@ public class DemoService { energy.setName("Energie"); energy.setUnit("kWh"); - final Series electricityEnergyPurchase = seriesRepository.findByName("eltern/electricity/energy/purchase").orElseThrow(); - final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); - electricityEnergyPurchaseGraph.setName("Bezug"); - electricityEnergyPurchaseGraph.setType(GraphType.BAR); - electricityEnergyPurchaseGraph.setStack("a"); - electricityEnergyPurchaseGraph.setColor("#FF8800"); - energy.addGraph(electricityEnergyPurchaseGraph); - final Series electricityEnergyDelivery = seriesRepository.findByName("eltern/electricity/energy/delivery").orElseThrow(); final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); electricityEnergyDeliveryGraph.setName("Überschuss"); @@ -268,11 +281,46 @@ public class DemoService { final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow(); final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); - electricityEnergyProduceGraph.setName("Produktion"); + electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery); + electricityEnergyProduceGraph.setName("Eigenverbrauch"); electricityEnergyProduceGraph.setType(GraphType.BAR); electricityEnergyProduceGraph.setStack("a"); - electricityEnergyProduceGraph.setColor("#0000FF"); + electricityEnergyProduceGraph.setColor("#008800"); energy.addGraph(electricityEnergyProduceGraph); + + final Series electricityEnergyPurchase = seriesRepository.findByName("eltern/electricity/energy/purchase").orElseThrow(); + final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase)); + electricityEnergyPurchaseGraph.setName("Bezug"); + electricityEnergyPurchaseGraph.setType(GraphType.BAR); + electricityEnergyPurchaseGraph.setStack("a"); + electricityEnergyPurchaseGraph.setColor("#FF8800"); + energy.addGraph(electricityEnergyPurchaseGraph); + } + + private void leistungVergleich() { + final Plot plot = plotRepository.save(new Plot(plotRepository.count())); + plot.setName("Leistung Vergleich"); + + final Axis power = axisRepository.save(new Axis(plot)); + plot.addAxis(power); + plot.setDashboard(true); + power.setRight(true); + power.setName("Leistung"); + power.setUnit("W"); + + final Series electricityPowerProduce = seriesRepository.findByName("electricity/power/produce").orElseThrow(); + final Graph electricityPowerProduceGraph = graphRepository.save(new Graph(power, electricityPowerProduce)); + electricityPowerProduceGraph.setName("Zuhause"); + electricityPowerProduceGraph.setType(GraphType.BAR); + electricityPowerProduceGraph.setColor("#008800"); + power.addGraph(electricityPowerProduceGraph); + + final Series elternElectricityPowerProduce = seriesRepository.findByName("eltern/electricity/power/produce").orElseThrow(); + final Graph elternElectricityPowerProduceGraph = graphRepository.save(new Graph(power, elternElectricityPowerProduce)); + elternElectricityPowerProduceGraph.setName("Eltern"); + elternElectricityPowerProduceGraph.setType(GraphType.BAR); + elternElectricityPowerProduceGraph.setColor("#0088FF"); + power.addGraph(elternElectricityPowerProduceGraph); } @NonNull diff --git a/src/main/java/de/ph87/data/Helpers.java b/src/main/java/de/ph87/data/Helpers.java index 899c894..59b8d12 100644 --- a/src/main/java/de/ph87/data/Helpers.java +++ b/src/main/java/de/ph87/data/Helpers.java @@ -3,6 +3,7 @@ package de.ph87.data; import jakarta.annotation.Nullable; import lombok.NonNull; +import java.util.function.BiFunction; import java.util.function.Function; public class Helpers { @@ -15,6 +16,14 @@ public class Helpers { return mapper.apply(t); } + @Nullable + public static R map(@Nullable final T t, @NonNull final U u, @NonNull final BiFunction mapper) { + if (t == null) { + return null; + } + return mapper.apply(t, u); + } + @NonNull public static T or(@Nullable final T t, @NonNull final T r) { if (t == null) { diff --git a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java index 145f788..d0fc775 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java @@ -2,6 +2,7 @@ package de.ph87.data.plot.axis.graph; import de.ph87.data.plot.axis.Axis; import de.ph87.data.series.Series; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -41,6 +42,24 @@ public class Graph { @ManyToOne(optional = false) private Series series; + @Setter + @Column(nullable = false) + private double factor = 1; + + @Setter + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private GraphOperation operation = GraphOperation.MINUS; + + @Setter + @Nullable + @ManyToOne + private Series series2; + + @Setter + @Column(nullable = false) + private double factor2 = 1; + @Setter @NonNull @Column(nullable = false) @@ -66,10 +85,6 @@ public class Graph { @Column(nullable = false) private String color = "#FF0000"; - @Setter - @Column(nullable = false) - private double factor = 1; - @Setter @NonNull @Enumerated(EnumType.STRING) diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java index 61824e2..e8cb18c 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java @@ -56,16 +56,31 @@ public class GraphController { 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}/operation") + public PlotDto operation(@PathVariable final long id, @RequestBody @NonNull final String value) { + return graphService.set(id, graph -> graph.setOperation(GraphOperation.valueOf(value))); + } + + @PostMapping("{id}/series2") + public PlotDto series2(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long value) { + return graphService.set(id, graph -> graph.setSeries2(value == null ? null : seriesRepository.findById(value).orElseThrow())); + } + + @PostMapping("{id}/factor2") + public PlotDto factor2(@PathVariable final long id, @RequestBody final double value) { + return graphService.set(id, graph -> graph.setFactor2(value)); + } + + @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}/group") public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String value) { return graphService.set(id, graph -> graph.setGroup(Group.valueOf(value))); diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphDivisionByZero.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphDivisionByZero.java new file mode 100644 index 0000000..1fc34fc --- /dev/null +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphDivisionByZero.java @@ -0,0 +1,5 @@ +package de.ph87.data.plot.axis.graph; + +public class GraphDivisionByZero extends Exception { + +} diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java index 6d30ad5..239f0da 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java @@ -1,9 +1,12 @@ package de.ph87.data.plot.axis.graph; import de.ph87.data.series.SeriesDto; +import jakarta.annotation.Nullable; import lombok.Data; import lombok.NonNull; +import static de.ph87.data.Helpers.map; + @Data public class GraphDto { @@ -14,6 +17,15 @@ public class GraphDto { @NonNull public final SeriesDto series; + public final double factor; + + public final GraphOperation operation; + + @Nullable + public final SeriesDto series2; + + public final double factor2; + @NonNull public final String name; @@ -26,8 +38,6 @@ public class GraphDto { @NonNull public final String color; - public final double factor; - @NonNull public final Group group; @@ -44,12 +54,15 @@ public class GraphDto { this.id = graph.getId(); this.version = graph.getVersion(); this.series = new SeriesDto(graph.getSeries(), false); + this.factor = graph.getFactor(); + this.operation = graph.getOperation(); + this.series2 = map(graph.getSeries2(), false, SeriesDto::new); + this.factor2 = graph.getFactor2(); this.name = graph.getName(); this.visible = graph.isVisible(); this.type = graph.getType(); this.fill = graph.getFill(); this.color = graph.getColor(); - this.factor = graph.getFactor(); this.group = graph.getGroup(); this.stack = graph.getStack(); this.min = graph.isMin(); diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphOperation.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphOperation.java new file mode 100644 index 0000000..c9178dc --- /dev/null +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphOperation.java @@ -0,0 +1,27 @@ +package de.ph87.data.plot.axis.graph; + +import lombok.NonNull; + +public enum GraphOperation { + MINUS((s0, s1) -> s0 - s1), + PLUS(Double::sum), + DIVIDE((s0, s1) -> { + if (s1 == 0) { + throw new GraphDivisionByZero(); + } + return s0 / s1; + }), + ; + + @NonNull + private final GraphOperationFunction function; + + GraphOperation(@NonNull final GraphOperationFunction function) { + this.function = function; + } + + public double apply(final double a, final double b) throws GraphDivisionByZero { + return function.apply(a, b); + } + +} diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphOperationFunction.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphOperationFunction.java new file mode 100644 index 0000000..d232a42 --- /dev/null +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphOperationFunction.java @@ -0,0 +1,8 @@ +package de.ph87.data.plot.axis.graph; + +@FunctionalInterface +public interface GraphOperationFunction { + + R apply(A a, B b) throws GraphDivisionByZero; + +} diff --git a/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java b/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java index 51ace3a..4fd5662 100644 --- a/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java +++ b/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java @@ -22,7 +22,7 @@ public class AllSeriesPointResponse { public final SeriesDto series; @Nullable - public final SeriesPoint point; + public final SeriesPoint point; } diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java b/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java index 134ae6c..bb009be 100644 --- a/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java +++ b/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java @@ -2,7 +2,9 @@ package de.ph87.data.series; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.plot.axis.graph.GraphOperation; import de.ph87.data.series.data.Interval; +import jakarta.annotation.Nullable; import lombok.Data; import lombok.NonNull; @@ -13,6 +15,15 @@ public class OneSeriesPointsRequest implements ISeriesPointRequest { public final long id; + public final double factor; + + public final GraphOperation operation; + + @Nullable + public final Long id2; + + public final double factor2; + @NonNull public final Interval interval; @@ -30,11 +41,19 @@ public class OneSeriesPointsRequest implements ISeriesPointRequest { public OneSeriesPointsRequest( @JsonProperty("id") final long id, + @JsonProperty("factor") final double factor, + @JsonProperty("operation") final GraphOperation operation, + @JsonProperty("id2") @Nullable final Long id2, + @JsonProperty("factor2") final double factor2, @JsonProperty("interval") final Interval interval, @JsonProperty("offset") final long offset, @JsonProperty("duration") final long duration ) { this.id = id; + this.factor = factor; + this.operation = operation; + this.id2 = id2; + this.factor2 = factor2; this.interval = interval; this.offset = offset; this.duration = duration; diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java b/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java index ddeecc5..834d4ec 100644 --- a/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java +++ b/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java @@ -9,6 +9,6 @@ import java.util.List; @JsonSerialize(using = OneSeriesPointsResponseSerializer.class) public class OneSeriesPointsResponse { - public final List points; + public final List> points; } diff --git a/src/main/java/de/ph87/data/series/SeriesPoint.java b/src/main/java/de/ph87/data/series/SeriesPoint.java index 31fdf65..f14f255 100644 --- a/src/main/java/de/ph87/data/series/SeriesPoint.java +++ b/src/main/java/de/ph87/data/series/SeriesPoint.java @@ -1,11 +1,67 @@ package de.ph87.data.series; import com.fasterxml.jackson.core.JsonGenerator; +import de.ph87.data.plot.axis.graph.GraphDivisionByZero; +import de.ph87.data.plot.axis.graph.GraphOperation; +import lombok.NonNull; import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; -public interface SeriesPoint { +public interface SeriesPoint> { + + ZonedDateTime getDate(); + + double getValue(); void toJson(final JsonGenerator jsonGenerator) throws IOException; + T times(final double factor); + + T combine(@NonNull final SeriesPoint other, @NonNull final GraphOperation operation) throws GraphDivisionByZero; + + @NonNull + static List> combine(@NonNull final List> points1, @NonNull final List> points2, @NonNull final GraphOperation operation) { + if (points1.isEmpty() || points2.isEmpty()) { + return Collections.emptyList(); + } + final List> as = new ArrayList<>(points1); + final List> bs = new ArrayList<>(points2); + SeriesPoint a = as.removeFirst(); + SeriesPoint b = bs.removeFirst(); + final List> result = new ArrayList<>(Math.min(as.size(), bs.size())); + while (true) { + final int diff = a.getDate().compareTo(b.getDate()); + if (diff == 0) { + try { + result.add(a.combine(b, operation)); + } catch (GraphDivisionByZero e) { + // just not add + } + a = null; + b = null; + } else if (diff < 0) { + a = null; + } else { + b = null; + } + if (a == null) { + if (as.isEmpty()) { + break; + } + a = as.removeFirst(); + } + if (b == null) { + if (bs.isEmpty()) { + break; + } + b = bs.removeFirst(); + } + } + return result; + } + } diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index e09b267..2407fdc 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -3,6 +3,7 @@ package de.ph87.data.series; import de.ph87.data.series.data.bool.BoolService; import de.ph87.data.series.data.delta.DeltaService; import de.ph87.data.series.data.varying.VaryingService; +import jakarta.annotation.Nullable; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,8 +28,14 @@ public class SeriesService { @NonNull public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) { - final Series series = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - return new OneSeriesPointsResponse(getSeriesPoints(series, request)); + final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final List> points1 = getSeriesPoints(series1, request, request.factor); + if (request.id2 != null) { + final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final List> points2 = getSeriesPoints(series2, request, request.factor2); + return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation)); + } + return new OneSeriesPointsResponse(points1); } @NonNull @@ -39,19 +46,23 @@ public class SeriesService { @NonNull private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { - final List points = getSeriesPoints(series, request); + final List> points = getSeriesPoints(series, request, null); final SeriesDto seriesDto = new SeriesDto(series, false); - final SeriesPoint point = points.isEmpty() ? null : points.getFirst(); + final SeriesPoint point = points.isEmpty() ? null : points.getFirst(); return new AllSeriesPointResponse.Entry(seriesDto, point); } @NonNull - private List getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { - return switch (series.getType()) { + private List> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) { + final List> points = switch (series.getType()) { case BOOL -> boolService.points(series, request); case DELTA -> deltaService.points(series, request); case VARYING -> varyingService.points(series, request); }; + if (factor == null || factor == 1) { + return points; + } + return points.stream().map(p -> p.times(factor)).toList(); } } diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java b/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java index 96ae7c2..fb4a47e 100644 --- a/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java +++ b/src/main/java/de/ph87/data/series/data/bool/BoolPoint.java @@ -1,14 +1,17 @@ package de.ph87.data.series.data.bool; import com.fasterxml.jackson.core.JsonGenerator; +import de.ph87.data.plot.axis.graph.GraphDivisionByZero; +import de.ph87.data.plot.axis.graph.GraphOperation; import de.ph87.data.series.SeriesPoint; +import lombok.Data; import lombok.NonNull; import java.io.IOException; import java.time.ZonedDateTime; -@SuppressWarnings("unused") // used by repository query -public class BoolPoint implements SeriesPoint { +@Data +public class BoolPoint implements SeriesPoint { public final ZonedDateTime begin; @@ -25,6 +28,13 @@ public class BoolPoint implements SeriesPoint { this.terminated = bool.isTerminated(); } + public BoolPoint(final ZonedDateTime begin, final ZonedDateTime end, final boolean state, final boolean terminated) { + this.begin = begin; + this.end = end; + this.state = state; + this.terminated = terminated; + } + @Override public void toJson(final JsonGenerator jsonGenerator) throws IOException { jsonGenerator.writeNumber(begin.toEpochSecond()); @@ -33,4 +43,24 @@ public class BoolPoint implements SeriesPoint { jsonGenerator.writeNumber(terminated ? 1 : 0); } + @Override + public BoolPoint times(final double factor) { + return this; + } + + @Override + public ZonedDateTime getDate() { + return begin; + } + + @Override + public double getValue() { + return state ? 1 : 0; + } + + @Override + public BoolPoint combine(@NonNull final SeriesPoint other, @NonNull final GraphOperation operation) throws GraphDivisionByZero { + return new BoolPoint(begin, end, operation.apply(getValue(), other.getValue()) > 0, terminated); + } + } diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java index 23100c8..1877f52 100644 --- a/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java +++ b/src/main/java/de/ph87/data/series/data/delta/DeltaPoint.java @@ -1,14 +1,17 @@ package de.ph87.data.series.data.delta; import com.fasterxml.jackson.core.JsonGenerator; +import de.ph87.data.plot.axis.graph.GraphDivisionByZero; +import de.ph87.data.plot.axis.graph.GraphOperation; import de.ph87.data.series.SeriesPoint; +import lombok.Data; import lombok.NonNull; import java.io.IOException; import java.time.ZonedDateTime; -@SuppressWarnings("unused") // used by repository query -public class DeltaPoint implements SeriesPoint { +@Data +public class DeltaPoint implements SeriesPoint { @NonNull public final ZonedDateTime date; @@ -26,4 +29,19 @@ public class DeltaPoint implements SeriesPoint { jsonGenerator.writeNumber(delta); } + @Override + public DeltaPoint times(final double factor) { + return new DeltaPoint(date, delta * factor); + } + + @Override + public double getValue() { + return delta; + } + + @Override + public DeltaPoint combine(@NonNull final SeriesPoint other, @NonNull final GraphOperation operation) throws GraphDivisionByZero { + return new DeltaPoint(date, operation.apply(getValue(), other.getValue())); + } + } diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java b/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java index a545df5..acb2898 100644 --- a/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java +++ b/src/main/java/de/ph87/data/series/data/delta/DeltaRepo.java @@ -13,7 +13,7 @@ import java.util.List; public interface DeltaRepo extends CrudRepository { @NonNull - @Query("select new de.ph87.data.series.data.delta.DeltaPoint(e.id.date, sum(e.last - e.first)) from #{#entityName} e where e.id.meter.series = :series and e.id.date >= :first and e.id.date < :after group by e.id.date") + @Query("select new de.ph87.data.series.data.delta.DeltaPoint(e.id.date, sum((e.last - e.first))) from #{#entityName} e where e.id.meter.series = :series and e.id.date >= :first and e.id.date < :after group by e.id.date") List points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after); } diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java b/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java index 37fe3d5..e5e3d54 100644 --- a/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java +++ b/src/main/java/de/ph87/data/series/data/varying/VaryingPoint.java @@ -1,14 +1,17 @@ package de.ph87.data.series.data.varying; import com.fasterxml.jackson.core.JsonGenerator; +import de.ph87.data.plot.axis.graph.GraphDivisionByZero; +import de.ph87.data.plot.axis.graph.GraphOperation; import de.ph87.data.series.SeriesPoint; +import lombok.Data; import lombok.NonNull; import java.io.IOException; import java.time.ZonedDateTime; -@SuppressWarnings("unused") // used by repository query -public class VaryingPoint implements SeriesPoint { +@Data +public class VaryingPoint implements SeriesPoint { public final ZonedDateTime date; @@ -25,6 +28,13 @@ public class VaryingPoint implements SeriesPoint { this.avg = varying.getAvg(); } + private VaryingPoint(final ZonedDateTime date, final double min, final double avg, final double max) { + this.date = date; + this.min = min; + this.avg = avg; + this.max = max; + } + @Override public void toJson(final JsonGenerator jsonGenerator) throws IOException { jsonGenerator.writeNumber(date.toEpochSecond()); @@ -33,4 +43,22 @@ public class VaryingPoint implements SeriesPoint { jsonGenerator.writeNumber(avg); } + @Override + public VaryingPoint times(final double factor) { + return new VaryingPoint(date, min * factor, avg * factor, max * factor); + } + + @Override + public double getValue() { + return avg; + } + + @Override + public VaryingPoint combine(@NonNull final SeriesPoint other, @NonNull final GraphOperation operation) throws GraphDivisionByZero { + if (other instanceof final VaryingPoint otherVarying) { + return new VaryingPoint(date, operation.apply(min, otherVarying.min), operation.apply(avg, otherVarying.avg), operation.apply(max, otherVarying.max)); + } + return new VaryingPoint(date, operation.apply(min, other.getValue()), operation.apply(avg, other.getValue()), operation.apply(max, other.getValue())); + } + }