EntryController + Graph
This commit is contained in:
parent
551db4b93d
commit
908134600c
@ -5,7 +5,7 @@ spring.datasource.driverClassName=org.h2.Driver
|
|||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=password
|
spring.datasource.password=password
|
||||||
#-
|
#-
|
||||||
spring.jpa.hibernate.ddl-auto=create
|
#spring.jpa.hibernate.ddl-auto=create
|
||||||
#-
|
#-
|
||||||
de.ph87.data.message.receive.mqtt.host=10.0.0.50
|
de.ph87.data.message.receive.mqtt.host=10.0.0.50
|
||||||
de.ph87.data.message.receive.mqtt.topic=#
|
de.ph87.data.message.receive.mqtt.topic=#
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.*;
|
|||||||
import com.fasterxml.jackson.databind.annotation.*;
|
import com.fasterxml.jackson.databind.annotation.*;
|
||||||
import de.ph87.data.message.*;
|
import de.ph87.data.message.*;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
import de.ph87.data.unit.Value;
|
import de.ph87.data.unit.Value;
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -25,7 +26,7 @@ public class EspHomeHandler implements IMessageHandler {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final EntryService entryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(@NonNull final Message message) throws Exception {
|
public void handle(@NonNull final Message message) throws Exception {
|
||||||
@ -58,7 +59,7 @@ public class EspHomeHandler implements IMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Value value = new Value(inbound.value, unitFromPayload);
|
final Value value = new Value(inbound.value, unitFromPayload);
|
||||||
seriesService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit)));
|
entryService.receive(new SeriesInbound(name, inbound.date, value.as(targetUnit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String propertyReplace(final String property) {
|
private String propertyReplace(final String property) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.ph87.data.message.handler;
|
|||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import de.ph87.data.message.*;
|
import de.ph87.data.message.*;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
import de.ph87.data.unit.Value;
|
import de.ph87.data.unit.Value;
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -21,7 +22,7 @@ public class HeizungHandler implements IMessageHandler {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final EntryService entryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(@NonNull final Message message) throws Exception {
|
public void handle(@NonNull final Message message) throws Exception {
|
||||||
@ -31,7 +32,7 @@ public class HeizungHandler implements IMessageHandler {
|
|||||||
}
|
}
|
||||||
final String property = matcher.group("property");
|
final String property = matcher.group("property");
|
||||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||||
seriesService.receive(new SeriesInbound(property, inbound.date, inbound.value));
|
entryService.receive(new SeriesInbound(property, inbound.date, inbound.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.ph87.data.message.handler;
|
|||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import de.ph87.data.message.*;
|
import de.ph87.data.message.*;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
import de.ph87.data.unit.Value;
|
import de.ph87.data.unit.Value;
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -18,7 +19,7 @@ public class OpenDTUHandler implements IMessageHandler {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final EntryService entryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(final @NonNull Message message) throws Exception {
|
public void handle(final @NonNull Message message) throws Exception {
|
||||||
@ -26,11 +27,10 @@ public class OpenDTUHandler implements IMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||||
seriesService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Unit.ENERGY_KWH)));
|
entryService.receive(new SeriesInbound("electricity/energy/produced", inbound.date, inbound.energy.as(Unit.ENERGY_KWH)));
|
||||||
seriesService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Unit.POWER_W)));
|
entryService.receive(new SeriesInbound("electricity/power/produced", inbound.date, inbound.power.as(Unit.POWER_W)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
private static class Inbound {
|
private static class Inbound {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.ph87.data.message.handler;
|
|||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import de.ph87.data.message.*;
|
import de.ph87.data.message.*;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
import de.ph87.data.unit.Value;
|
import de.ph87.data.unit.Value;
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -18,7 +19,7 @@ public class SimpleJsonHandler implements IMessageHandler {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final EntryService entryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(@NonNull final Message message) throws Exception {
|
public void handle(@NonNull final Message message) throws Exception {
|
||||||
@ -26,7 +27,7 @@ public class SimpleJsonHandler implements IMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||||
seriesService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value));
|
entryService.receive(new SeriesInbound(inbound.name, inbound.date, inbound.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.ph87.data.message.handler;
|
|||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import de.ph87.data.message.*;
|
import de.ph87.data.message.*;
|
||||||
import de.ph87.data.series.*;
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
import de.ph87.data.unit.Value;
|
import de.ph87.data.unit.Value;
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -18,7 +19,7 @@ public class SmartMeterHandler implements IMessageHandler {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final SeriesService seriesService;
|
private final EntryService entryService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(@NonNull final Message message) throws Exception {
|
public void handle(@NonNull final Message message) throws Exception {
|
||||||
@ -26,9 +27,9 @@ public class SmartMeterHandler implements IMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
final Inbound inbound = objectMapper.readValue(message.payload, Inbound.class);
|
||||||
seriesService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Unit.ENERGY_KWH)));
|
entryService.receive(new SeriesInbound("electricity/energy/purchased", inbound.date, inbound.energyPurchased.as(Unit.ENERGY_KWH)));
|
||||||
seriesService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Unit.ENERGY_KWH)));
|
entryService.receive(new SeriesInbound("electricity/energy/delivered", inbound.date, inbound.energyDelivered.as(Unit.ENERGY_KWH)));
|
||||||
seriesService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Unit.POWER_W)));
|
entryService.receive(new SeriesInbound("electricity/power/difference", inbound.date, inbound.powerDifference.as(Unit.POWER_W)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@ -30,6 +30,10 @@ public class Series {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private Unit unit;
|
private Unit unit;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int decimals = 1;
|
||||||
|
|
||||||
public Series(@NonNull final String name, @NonNull final Unit unit) {
|
public Series(@NonNull final String name, @NonNull final Unit unit) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.title = name;
|
this.title = name;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
|
import jakarta.annotation.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -15,11 +16,21 @@ public class SeriesDto {
|
|||||||
|
|
||||||
public final Unit unit;
|
public final Unit unit;
|
||||||
|
|
||||||
|
public final int decimals;
|
||||||
|
|
||||||
public SeriesDto(@NonNull final Series series) {
|
public SeriesDto(@NonNull final Series series) {
|
||||||
this.id = series.getId();
|
this.id = series.getId();
|
||||||
this.name = series.getName();
|
this.name = series.getName();
|
||||||
this.title = series.getTitle();
|
this.title = series.getTitle();
|
||||||
this.unit = series.getUnit();
|
this.unit = series.getUnit();
|
||||||
|
this.decimals = series.getDecimals();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String format(@Nullable final Double value) {
|
||||||
|
if (value == null || Double.isNaN(value)) {
|
||||||
|
return "--- %s".formatted(unit.unit);
|
||||||
|
}
|
||||||
|
return "%%.%df %%s".formatted(decimals).formatted(value, unit.unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package de.ph87.data.series;
|
package de.ph87.data.series;
|
||||||
|
|
||||||
import de.ph87.data.*;
|
import de.ph87.data.*;
|
||||||
import de.ph87.data.series.entry.*;
|
|
||||||
import de.ph87.data.unit.*;
|
import de.ph87.data.unit.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.extern.slf4j.*;
|
import lombok.extern.slf4j.*;
|
||||||
@ -18,22 +17,28 @@ public class SeriesService {
|
|||||||
|
|
||||||
private final SeriesRepository seriesRepository;
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
private final EntryService entryService;
|
@SuppressWarnings("unused")
|
||||||
|
public SeriesDto modify(final long id, @NonNull final Consumer<Series> modifier) {
|
||||||
public SeriesDto modify(@NonNull final long id, @NonNull final Consumer<Series> modifier) {
|
|
||||||
final Series series = getById(id);
|
final Series series = getById(id);
|
||||||
modifier.accept(series);
|
modifier.accept(series);
|
||||||
return publish(series, Action.CHANGED);
|
return publish(series, Action.CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(@NonNull final long id) {
|
@SuppressWarnings("unused")
|
||||||
|
public void delete(final long id) {
|
||||||
final Series series = getById(id);
|
final Series series = getById(id);
|
||||||
|
seriesRepository.delete(series);
|
||||||
|
publish(series, Action.DELETED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Series getById(@NonNull final long id) {
|
private Series getById(final long id) {
|
||||||
return seriesRepository.findById(id).orElseThrow();
|
return seriesRepository.findById(id).orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SeriesDto getDtoById(final long id) {
|
||||||
|
return toDto(getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
|
private SeriesDto publish(@NonNull final Series series, @NonNull final Action action) {
|
||||||
final SeriesDto dto = toDto(series);
|
final SeriesDto dto = toDto(series);
|
||||||
log.info("Series {}: {}", action, series);
|
log.info("Series {}: {}", action, series);
|
||||||
@ -44,19 +49,14 @@ public class SeriesService {
|
|||||||
return new SeriesDto(series);
|
return new SeriesDto(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void receive(@NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
public Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) {
|
||||||
final Series series = getOrCreate(measure.name, measure.value.unit);
|
|
||||||
entryService.write(series, measure);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Series getOrCreate(@NonNull final String name, @NonNull final Unit unit) {
|
|
||||||
return seriesRepository
|
return seriesRepository
|
||||||
.findByName(name)
|
.findByName(name)
|
||||||
.orElseGet(() -> {
|
.orElseGet(() -> {
|
||||||
final Series series = seriesRepository.save(new Series(name, unit));
|
final Series series = seriesRepository.save(new Series(name, unit));
|
||||||
publish(series, Action.CREATED);
|
publish(series, Action.CREATED);
|
||||||
return series;
|
return series;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,9 +14,9 @@ import java.time.temporal.*;
|
|||||||
@ToString
|
@ToString
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table(
|
@Table(
|
||||||
uniqueConstraints = {
|
uniqueConstraints = {
|
||||||
@UniqueConstraint(columnNames = {"series_id", "date"})
|
@UniqueConstraint(columnNames = {"series_id", "date"})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public class Entry {
|
public class Entry {
|
||||||
|
|
||||||
|
|||||||
@ -11,4 +11,6 @@ public interface EntryRepository extends ListCrudRepository<Entry, Long> {
|
|||||||
|
|
||||||
Optional<Entry> findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated);
|
Optional<Entry> findBySeriesAndDate(@NonNull Series series, @NonNull ZonedDateTime truncated);
|
||||||
|
|
||||||
|
List<Entry> findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(long id, @NonNull ZonedDateTime begin, @NonNull ZonedDateTime end);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,19 @@ public class EntryService {
|
|||||||
|
|
||||||
private final EntryRepository entryRepository;
|
private final EntryRepository entryRepository;
|
||||||
|
|
||||||
|
private final SeriesService seriesService;
|
||||||
|
|
||||||
public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
public void write(@NonNull final Series series, @NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
||||||
final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES);
|
final ZonedDateTime truncated = measure.date.truncatedTo(ChronoUnit.MINUTES).minusMinutes(measure.date.getMinute() % 5);
|
||||||
if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) {
|
if (entryRepository.findBySeriesAndDate(series, truncated).isEmpty()) {
|
||||||
final Entry created = entryRepository.save(new Entry(series, truncated, measure.value));
|
final Entry created = entryRepository.save(new Entry(series, truncated, measure.value));
|
||||||
log.debug("Created: {}", created);
|
log.debug("Created: {}", created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void receive(@NonNull final SeriesInbound measure) throws Unit.NotConvertible {
|
||||||
|
final Series series = seriesService.getOrCreate(measure.name, measure.value.unit);
|
||||||
|
write(series, measure);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
161
src/main/java/de/ph87/data/series/graph/Graph.java
Normal file
161
src/main/java/de/ph87/data/series/graph/Graph.java
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package de.ph87.data.series.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
|
import jakarta.annotation.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.*;
|
||||||
|
import java.awt.image.*;
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.*;
|
||||||
|
|
||||||
|
import static java.lang.Math.*;
|
||||||
|
|
||||||
|
public class Graph {
|
||||||
|
|
||||||
|
public static final Stroke NORMAL = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1);
|
||||||
|
|
||||||
|
public static final Stroke DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{2, 2}, 0);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final SeriesDto series;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime begin;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime end;
|
||||||
|
|
||||||
|
public final int width;
|
||||||
|
|
||||||
|
public final int height;
|
||||||
|
|
||||||
|
public final int border;
|
||||||
|
|
||||||
|
public final List<Point> points;
|
||||||
|
|
||||||
|
public final long minuteMin;
|
||||||
|
|
||||||
|
public final long minuteMax;
|
||||||
|
|
||||||
|
public final long minuteRange;
|
||||||
|
|
||||||
|
public final double minuteScale;
|
||||||
|
|
||||||
|
public final double valueMin;
|
||||||
|
|
||||||
|
public final double valueMax;
|
||||||
|
|
||||||
|
public final double valueAvg;
|
||||||
|
|
||||||
|
public final double valueRange;
|
||||||
|
|
||||||
|
public final double valueScale;
|
||||||
|
|
||||||
|
public final int heightInner;
|
||||||
|
|
||||||
|
public final int widthInner;
|
||||||
|
|
||||||
|
public final int maxLabelWidth;
|
||||||
|
|
||||||
|
public Graph(@NonNull final SeriesDto series, @NonNull final List<Entry> entries, final @NonNull ZonedDateTime begin, final @NonNull ZonedDateTime end, final int width, final int height, final int border) {
|
||||||
|
this.series = series;
|
||||||
|
this.begin = begin;
|
||||||
|
this.end = end;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.border = border;
|
||||||
|
|
||||||
|
double vSum = 0;
|
||||||
|
double vMin = Double.MAX_VALUE;
|
||||||
|
double vMax = Double.MIN_VALUE;
|
||||||
|
int maxLabelWidth = 80;
|
||||||
|
final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics();
|
||||||
|
for (final Entry entry : entries) {
|
||||||
|
vMin = Math.min(vMin, entry.getValue());
|
||||||
|
vMax = max(vMax, entry.getValue());
|
||||||
|
vSum += entry.getValue();
|
||||||
|
maxLabelWidth = max(maxLabelWidth, fontMetrics.stringWidth(series.format(entry.getValue())));
|
||||||
|
}
|
||||||
|
this.valueAvg = vSum / entries.size();
|
||||||
|
this.maxLabelWidth = maxLabelWidth;
|
||||||
|
|
||||||
|
widthInner = width - 3 * border - maxLabelWidth;
|
||||||
|
heightInner = height - 2 * border;
|
||||||
|
|
||||||
|
minuteMin = begin.toEpochSecond() / 60;
|
||||||
|
minuteMax = end.toEpochSecond() / 60;
|
||||||
|
minuteRange = minuteMax - minuteMin;
|
||||||
|
minuteScale = (double) widthInner / minuteRange;
|
||||||
|
|
||||||
|
valueMin = vMin;
|
||||||
|
valueMax = vMax;
|
||||||
|
valueRange = vMax - vMin;
|
||||||
|
valueScale = heightInner / valueRange;
|
||||||
|
|
||||||
|
points = entries.stream().map(toPoint(minuteMin, minuteScale, vMin, valueScale)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BufferedImage draw() {
|
||||||
|
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
final Graphics2D g = (Graphics2D) image.getGraphics();
|
||||||
|
final int fontH3_4 = (int) Math.round(g.getFontMetrics().getHeight() * 0.75);
|
||||||
|
|
||||||
|
g.setColor(Color.gray);
|
||||||
|
final String string = "%s [%s]".formatted(series.getTitle(), series.unit.unit);
|
||||||
|
g.drawString(string, border, border + fontH3_4);
|
||||||
|
|
||||||
|
yLabel(g, valueMax, DASHED, Color.red);
|
||||||
|
yLabel(g, valueAvg, DASHED, new Color(0, 127, 0));
|
||||||
|
yLabel(g, 0, NORMAL, Color.BLACK);
|
||||||
|
yLabel(g, valueMin, DASHED, Color.blue);
|
||||||
|
|
||||||
|
g.translate(border, height - border);
|
||||||
|
g.scale(1, -1);
|
||||||
|
|
||||||
|
g.setStroke(NORMAL);
|
||||||
|
g.setColor(Color.BLACK);
|
||||||
|
g.drawLine(widthInner, 0, widthInner, heightInner); // y-axis
|
||||||
|
|
||||||
|
Point last = null;
|
||||||
|
g.setColor(Color.RED);
|
||||||
|
for (final Point current : points) {
|
||||||
|
if (last != null && current.x - last.x <= minuteScale * 2) {
|
||||||
|
g.drawLine(last.x, last.y, current.x, current.y);
|
||||||
|
}
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void yLabel(final Graphics2D g, final double value, @Nullable final Stroke stroke, final @Nullable Color color) {
|
||||||
|
final String string = series.format(value);
|
||||||
|
final int offset = maxLabelWidth - g.getFontMetrics().stringWidth(string);
|
||||||
|
final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border);
|
||||||
|
g.setColor(color);
|
||||||
|
g.drawString(string, widthInner + 2 * border + offset, y + (int) Math.round(g.getFontMetrics().getHeight() * 0.25));
|
||||||
|
if (stroke != null && color != null) {
|
||||||
|
g.setStroke(stroke);
|
||||||
|
g.draw(new Line2D.Double(border, y, width - maxLabelWidth - border * 1.5, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Entry, Point> toPoint(final long minuteMin, final double minuteScale, final double valueMin, final double valueScale) {
|
||||||
|
return entry -> {
|
||||||
|
final long minuteEpoch = entry.getDate().toEpochSecond() / 60;
|
||||||
|
final long minuteRelative = minuteEpoch - minuteMin;
|
||||||
|
final double minuteScaled = minuteRelative * minuteScale;
|
||||||
|
final int x = (int) Math.round(minuteScaled);
|
||||||
|
|
||||||
|
final double valueRelative = entry.getValue() - valueMin;
|
||||||
|
final double valueScaled = valueRelative * valueScale;
|
||||||
|
final int y = (int) Math.round(valueScaled);
|
||||||
|
|
||||||
|
return new Point(x, y);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/java/de/ph87/data/series/graph/GraphController.java
Normal file
32
src/main/java/de/ph87/data/series/graph/GraphController.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package de.ph87.data.series.graph;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.*;
|
||||||
|
import lombok.*;
|
||||||
|
import lombok.extern.slf4j.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.imageio.*;
|
||||||
|
import java.awt.image.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.time.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Series/Graph")
|
||||||
|
public class GraphController {
|
||||||
|
|
||||||
|
private final GraphService graphService;
|
||||||
|
|
||||||
|
@GetMapping(path = "{seriesId}/{width}/{height}/{offset}/{duration}", produces = "image/png")
|
||||||
|
public void graph(@PathVariable final long seriesId, final HttpServletResponse response, @PathVariable final int width, @PathVariable final int height, @PathVariable final String offset, @PathVariable final String duration) throws IOException {
|
||||||
|
final ZonedDateTime end = ZonedDateTime.now().minus(Duration.parse(offset));
|
||||||
|
final ZonedDateTime begin = end.minus(Duration.parse(duration));
|
||||||
|
final Graph graph = graphService.getGraph(seriesId, begin, end, width, height, 10);
|
||||||
|
final BufferedImage image = graph.draw();
|
||||||
|
response.setContentType("image/png");
|
||||||
|
ImageIO.write(image, "PNG", response.getOutputStream());
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
src/main/java/de/ph87/data/series/graph/GraphService.java
Normal file
29
src/main/java/de/ph87/data/series/graph/GraphService.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package de.ph87.data.series.graph;
|
||||||
|
|
||||||
|
import de.ph87.data.series.*;
|
||||||
|
import de.ph87.data.series.entry.*;
|
||||||
|
import lombok.*;
|
||||||
|
import lombok.extern.slf4j.*;
|
||||||
|
import org.springframework.stereotype.*;
|
||||||
|
import org.springframework.transaction.annotation.*;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GraphService {
|
||||||
|
|
||||||
|
private final EntryRepository entryRepository;
|
||||||
|
|
||||||
|
private final SeriesService seriesService;
|
||||||
|
|
||||||
|
public Graph getGraph(final long seriesId, @NonNull final ZonedDateTime begin, @NonNull final ZonedDateTime end, final int width, final int height, final int border) {
|
||||||
|
final SeriesDto series = seriesService.getDtoById(seriesId);
|
||||||
|
final List<Entry> entries = entryRepository.findAllBySeriesIdAndDateGreaterThanEqualAndDateLessThanEqual(seriesId, begin, end);
|
||||||
|
return new Graph(series, entries, begin, end, width, height, border);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ public enum Unit {
|
|||||||
IAQ_CO2_EQUIVALENT("ppm"),
|
IAQ_CO2_EQUIVALENT("ppm"),
|
||||||
IAQ_VOC_EQUIVALENT("ppm"),
|
IAQ_VOC_EQUIVALENT("ppm"),
|
||||||
SUN_DC("Δ°C"),
|
SUN_DC("Δ°C"),
|
||||||
|
UNIT_PERCENT("%"),
|
||||||
;
|
;
|
||||||
|
|
||||||
public final String unit;
|
public final String unit;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user