View backend + readonly frontend

This commit is contained in:
Patrick Haßel 2025-05-06 12:34:19 +02:00
parent 59b9a5f44f
commit 54e1487300
42 changed files with 1136 additions and 151 deletions

View File

@ -18,7 +18,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<version>3.4.4</version>
</parent>
<dependencies>

View File

@ -2,6 +2,7 @@
@foreground: gray;
@background: white;
@FONT_SELECTABLE: white;
@consumption: orange;
@purchase: orangered;

View File

@ -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;
}
}

View File

@ -0,0 +1,49 @@
<ng-container *ngIf="view && seriesList">
<div [ngSwitch]="view._type_">
<ng-container *ngSwitchCase="'literal'">
<input type="number" [ngModel]="ViewLiteral.cast(view).value">
</ng-container>
<ng-container *ngSwitchCase="'series'">
<select [ngModel]="ViewSeries.cast(view).series.id">
<option *ngFor="let series of seriesList" [ngValue]="series.id">{{ series.name }}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'unary'">
<select [ngModel]="ViewUnary.cast(view).operation">
<option [ngValue]="ViewUnaryOperator.NEG">negiere</option>
<option [ngValue]="ViewUnaryOperator.NOT_NEG">nicht negativ</option>
<option [ngValue]="ViewUnaryOperator.REC">Kehrwert</option>
</select>
<div class="children">
<div class="child">
<app-view-body [view]="ViewUnary.cast(view).view" [seriesList]="seriesList"></app-view-body>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'binary'">
<div class="children">
<div class="child">
<app-view-body [view]="ViewBinary.cast(view).view0" [seriesList]="seriesList"></app-view-body>
</div>
<select class="binary" [ngModel]="ViewBinary.cast(view).operation">
<option [ngValue]="ViewBinaryOperator.PLUS">plus</option>
<option [ngValue]="ViewBinaryOperator.MINUS">minus</option>
<option [ngValue]="ViewBinaryOperator.MULTIPLY">mal</option>
<option [ngValue]="ViewBinaryOperator.DIVIDE">geteilt</option>
<option [ngValue]="ViewBinaryOperator.MODULO">mod</option>
<option [ngValue]="ViewBinaryOperator.PERCENT">Prozent</option>
</select>
<div class="child">
<app-view-body [view]="ViewBinary.cast(view).view1" [seriesList]="seriesList"></app-view-body>
</div>
</div>
</ng-container>
</div>
</ng-container>

View File

@ -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;
}

View File

@ -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[] = [];
}

View File

@ -0,0 +1,11 @@
<div class="list">
<div class="view" *ngFor="let view of rootList">
<div class="labelPair">
<div class="name">Name:</div>
<input [ngModel]="view.name" placeholder="Name">
</div>
<div class="body">
<app-view-body [view]="view" [seriesList]="seriesList"></app-view-body>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<View> = json => View.fromJson(json, this.locale);
constructor(
readonly api: ApiService,
@Inject(LOCALE_ID) readonly locale: string,
) {
//
}
list(next: Next<View[]>) {
return this.api.getList(['View', 'rootList'], this.fromJson, next);
}
byUuid(uuid: string, next: Next<View>) {
return this.api.getSingle(['View', 'byUuid', uuid], this.fromJson, next);
}
}

View File

@ -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 {

View File

@ -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},
];

View File

@ -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'

View File

@ -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;

View File

@ -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<Point> points;
public final List<java.awt.Point> 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<GraphPoint> 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<Point> 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<GraphPoint, Point> toPoint() {
private Function<Point, java.awt.Point> 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);
};
}

View File

@ -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<Point> 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();
}
}

View File

@ -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);
}
}

View File

@ -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<Point> fetch(@RequestBody @NonNull final ViewPointRequest request) {
return viewService.getPoints(request);
}
}

View File

@ -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);
}
}

View File

@ -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<Point> 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);
};
}
}

View File

@ -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<Object> websocketTopic = List.of("Series");
public final long id;

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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<GraphPoint> 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);
}
}

View File

@ -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<GraphPoint> getPoints(@NonNull final SeriesDto series, @NonNull final Alignment alignment, @NonNull final Aligned begin, @NonNull final Aligned end) {
final List<? extends MeterValue> graphPoints = findRepository(alignment).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.id, begin.date, end.date);
final List<GraphPoint> points = graphPoints.stream().map(meterValue -> new GraphPoint(meterValue.getId().getDate(), meterValue.getMax() - meterValue.getMin())).collect(Collectors.toCollection(LinkedList::new));
public List<Point> getPoints(final @NonNull Series series, @NonNull final PointRequest pointRequest) {
final List<? extends MeterValue> graphPoints = findRepository(pointRequest.inner).findAllByIdMeterSeriesIdAndIdDateGreaterThanEqualAndIdDateLessThanEqualOrderByIdDate(series.getId(), pointRequest.begin.date, pointRequest.end.date);
final List<Point> 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));
}
}

View File

@ -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<GraphPoint> 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<Point> 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();
}

View File

@ -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<ViewDto> rootList() {
return viewRepository.findAllDtoByNameNotEmpty();
}
}

View File

@ -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;
}
}

View File

@ -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<View, String> {
boolean existsByName(@NonNull String name);
List<View> findAllByNameNot(@NonNull String name);
default List<ViewDto> findAllDtoByNameNotEmpty() {
return findAllByNameNot("").stream().map(ViewDto::map).toList();
}
}

View File

@ -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<Long, List<Point>> seriesPoints = new HashMap<>();
private final Map<Double, List<Point>> literalPoints = new HashMap<>();
@NonNull
public List<Point> getPoints(@NonNull final Series series) {
return seriesPoints.computeIfAbsent(series.getId(), ignore -> pointService.getPoints(series, request));
}
@NonNull
public List<Point> getLiteral(final double value) {
return literalPoints.computeIfAbsent(value, this::_generateLiteral);
}
@NonNull
private List<Point> _generateLiteral(final double value) {
final List<Point> 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;
}
}

View File

@ -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<Point> getPoints(@NonNull final ViewPointRequest request) {
final ViewScope scope = new ViewScope(pointService, request);
final View view = viewRepository.findById(request.viewUuid).orElseThrow();
return view.getPoints(scope);
}
}

View File

@ -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<Point> getPoints(final @NonNull ViewScope scope);
}

View File

@ -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<Double, Double, Double> function;
Operation(final BiFunction<Double, Double, Double> 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<Point> getPoints(final @NonNull ViewScope scope) {
final List<Point> pointsA = view0.getPoints(scope);
final List<Point> pointsB = view1.getPoints(scope);
final List<Point> 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;
}
}

View File

@ -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());
}
}

View File

@ -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());
};
}
}

View File

@ -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<Point> getPoints(final @NonNull ViewScope scope) {
return scope.getLiteral(value);
}
}

View File

@ -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();
}
}

View File

@ -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<Point> getPoints(final @NonNull ViewScope scope) {
return scope.getPoints(series);
}
}

View File

@ -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());
}
}

View File

@ -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<Double, Double> function;
Operation(final Function<Double, Double> function) {
this.function = function;
}
@NonNull
public Point apply(@NonNull final Point point) {
return new Point(point.date, function.apply(point.value));
}
}
@Override
public List<Point> getPoints(final @NonNull ViewScope scope) {
return view.getPoints(scope).stream().map(operation::apply).toList();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}