Add event-backed tachograph timeline mode
This commit is contained in:
parent
9ef8bfc412
commit
2ded38a28a
|
|
@ -356,12 +356,23 @@ public class EventHubProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Processing {
|
public static class Processing {
|
||||||
|
private TimelineInputMode timelineInputMode = TimelineInputMode.INTERVALS;
|
||||||
private int operatingSplitIdleHours = 7;
|
private int operatingSplitIdleHours = 7;
|
||||||
private int significantDrivingMinutes = 3;
|
private int significantDrivingMinutes = 3;
|
||||||
private int minimumRestPeriodMinutes = 720;
|
private int minimumRestPeriodMinutes = 720;
|
||||||
private int mergeGapSeconds = 0;
|
private int mergeGapSeconds = 0;
|
||||||
private int gapDetectionToleranceSeconds = 0;
|
private int gapDetectionToleranceSeconds = 0;
|
||||||
|
|
||||||
|
public TimelineInputMode getTimelineInputMode() {
|
||||||
|
return timelineInputMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimelineInputMode(TimelineInputMode timelineInputMode) {
|
||||||
|
if (timelineInputMode != null) {
|
||||||
|
this.timelineInputMode = timelineInputMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int getOperatingSplitIdleHours() {
|
public int getOperatingSplitIdleHours() {
|
||||||
return operatingSplitIdleHours;
|
return operatingSplitIdleHours;
|
||||||
}
|
}
|
||||||
|
|
@ -403,6 +414,11 @@ public class EventHubProperties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TimelineInputMode {
|
||||||
|
INTERVALS,
|
||||||
|
EVENTS
|
||||||
|
}
|
||||||
|
|
||||||
public static class LegalRequirements {
|
public static class LegalRequirements {
|
||||||
private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";
|
private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,427 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class EventBackedDriverTimelineBuilder {
|
||||||
|
|
||||||
|
private final DriverTimelineEventBuilder eventBuilder;
|
||||||
|
|
||||||
|
public EventBackedDriverTimelineBuilder(DriverTimelineEventBuilder eventBuilder) {
|
||||||
|
this.eventBuilder = eventBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolvedDriverTimeline build(
|
||||||
|
TachographFileSession session,
|
||||||
|
DriverExtractionSession driverSession
|
||||||
|
) {
|
||||||
|
TachographTimelineEventBundle bundle = eventBuilder.buildEventBundle(session, driverSession);
|
||||||
|
List<ResolvedActivityInterval> activityIntervals =
|
||||||
|
reconstructActivityIntervals(bundle.activityEvents());
|
||||||
|
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals =
|
||||||
|
reconstructVehicleUsageIntervals(session.sessionId(), driverSession.driverKey(), bundle.vehicleUsageEvents());
|
||||||
|
List<ExtractedSupportEvent> supportEvents =
|
||||||
|
reconstructSupportEvents(bundle.supportEvents());
|
||||||
|
List<ExtractionWarning> warnings = mergeWarnings(session.warnings(), driverSession.warnings());
|
||||||
|
|
||||||
|
OffsetDateTime loadedFrom = minTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
|
||||||
|
OffsetDateTime loadedTo = maxTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
|
||||||
|
return new ResolvedDriverTimeline(
|
||||||
|
resolveSourceKind(session, bundle),
|
||||||
|
loadedFrom,
|
||||||
|
loadedTo,
|
||||||
|
vehicleUsageIntervals,
|
||||||
|
activityIntervals,
|
||||||
|
supportEvents,
|
||||||
|
warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ResolvedActivityInterval> reconstructActivityIntervals(List<EventHubEventDto> activityEvents) {
|
||||||
|
if (activityEvents == null || activityEvents.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<String, ActivityAccumulator> byIntervalId = new LinkedHashMap<>();
|
||||||
|
for (EventHubEventDto event : activityEvents) {
|
||||||
|
if (!"DRIVER_ACTIVITY".equals(event.eventDomain().name())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JsonNode raw = raw(event);
|
||||||
|
String intervalId = text(raw, "intervalId");
|
||||||
|
if (intervalId == null) {
|
||||||
|
intervalId = text(raw, "sourceRowId");
|
||||||
|
}
|
||||||
|
if (intervalId == null) {
|
||||||
|
intervalId = event.externalSourceEventId();
|
||||||
|
}
|
||||||
|
String resolvedIntervalId = intervalId;
|
||||||
|
ActivityAccumulator accumulator = byIntervalId.computeIfAbsent(
|
||||||
|
resolvedIntervalId,
|
||||||
|
ignored -> new ActivityAccumulator(resolvedIntervalId)
|
||||||
|
);
|
||||||
|
accumulator.accept(event, raw);
|
||||||
|
}
|
||||||
|
List<ResolvedActivityInterval> result = new ArrayList<>(byIntervalId.size());
|
||||||
|
for (ActivityAccumulator accumulator : byIntervalId.values()) {
|
||||||
|
ResolvedActivityInterval interval = accumulator.finish();
|
||||||
|
if (interval != null) {
|
||||||
|
result.add(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sort(Comparator.comparing(ResolvedActivityInterval::from)
|
||||||
|
.thenComparing(ResolvedActivityInterval::to)
|
||||||
|
.thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ResolvedVehicleUsageInterval> reconstructVehicleUsageIntervals(
|
||||||
|
UUID sessionId,
|
||||||
|
String driverKey,
|
||||||
|
List<EventHubEventDto> vehicleUsageEvents
|
||||||
|
) {
|
||||||
|
if (vehicleUsageEvents == null || vehicleUsageEvents.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<String, VehicleUsageAccumulator> byIntervalId = new LinkedHashMap<>();
|
||||||
|
for (EventHubEventDto event : vehicleUsageEvents) {
|
||||||
|
if (!"DRIVER_CARD".equals(event.eventDomain().name())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JsonNode raw = raw(event);
|
||||||
|
String intervalId = text(raw, "intervalId");
|
||||||
|
if (intervalId == null) {
|
||||||
|
intervalId = text(raw, "sourceRowId");
|
||||||
|
}
|
||||||
|
if (intervalId == null) {
|
||||||
|
intervalId = event.externalSourceEventId();
|
||||||
|
}
|
||||||
|
String resolvedIntervalId = intervalId;
|
||||||
|
VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent(
|
||||||
|
resolvedIntervalId,
|
||||||
|
ignored -> new VehicleUsageAccumulator(sessionId, driverKey, resolvedIntervalId)
|
||||||
|
);
|
||||||
|
accumulator.accept(event, raw);
|
||||||
|
}
|
||||||
|
List<ResolvedVehicleUsageInterval> result = new ArrayList<>(byIntervalId.size());
|
||||||
|
for (VehicleUsageAccumulator accumulator : byIntervalId.values()) {
|
||||||
|
ResolvedVehicleUsageInterval interval = accumulator.finish();
|
||||||
|
if (interval != null) {
|
||||||
|
result.add(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sort(Comparator.comparing(ResolvedVehicleUsageInterval::from)
|
||||||
|
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ExtractedSupportEvent> reconstructSupportEvents(List<EventHubEventDto> supportEvents) {
|
||||||
|
if (supportEvents == null || supportEvents.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<ExtractedSupportEvent> result = new ArrayList<>(supportEvents.size());
|
||||||
|
for (EventHubEventDto event : supportEvents) {
|
||||||
|
JsonNode raw = raw(event);
|
||||||
|
String eventId = text(raw, "supportEventId");
|
||||||
|
if (eventId == null) {
|
||||||
|
eventId = event.externalSourceEventId();
|
||||||
|
}
|
||||||
|
BigDecimal latitude = event.position() == null ? null : event.position().latitude();
|
||||||
|
BigDecimal longitude = event.position() == null ? null : event.position().longitude();
|
||||||
|
result.add(new ExtractedSupportEvent(
|
||||||
|
eventId,
|
||||||
|
event.occurredAt(),
|
||||||
|
event.eventDomain().name(),
|
||||||
|
text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),
|
||||||
|
text(raw, "slot"),
|
||||||
|
text(raw, "registrationKey"),
|
||||||
|
text(raw, "vehicleKey"),
|
||||||
|
text(raw, "country"),
|
||||||
|
text(raw, "region"),
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
text(raw, "authenticationStatus"),
|
||||||
|
longValue(raw, "odometerKm"),
|
||||||
|
text(raw, "code"),
|
||||||
|
text(raw, "rawRecordPath")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
result.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
|
||||||
|
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||||
|
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ExtractionWarning> mergeWarnings(List<ExtractionWarning> sessionWarnings, List<ExtractionWarning> driverWarnings) {
|
||||||
|
LinkedHashSet<ExtractionWarning> merged = new LinkedHashSet<>();
|
||||||
|
if (sessionWarnings != null) {
|
||||||
|
merged.addAll(sessionWarnings);
|
||||||
|
}
|
||||||
|
if (driverWarnings != null) {
|
||||||
|
merged.addAll(driverWarnings);
|
||||||
|
}
|
||||||
|
return List.copyOf(merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime minTimestamp(
|
||||||
|
List<ResolvedActivityInterval> activityIntervals,
|
||||||
|
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||||
|
List<ExtractedSupportEvent> supportEvents
|
||||||
|
) {
|
||||||
|
OffsetDateTime min = null;
|
||||||
|
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||||
|
min = min(min, interval.from());
|
||||||
|
}
|
||||||
|
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||||
|
min = min(min, interval.from());
|
||||||
|
}
|
||||||
|
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||||
|
min = min(min, supportEvent.occurredAt());
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime maxTimestamp(
|
||||||
|
List<ResolvedActivityInterval> activityIntervals,
|
||||||
|
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||||
|
List<ExtractedSupportEvent> supportEvents
|
||||||
|
) {
|
||||||
|
OffsetDateTime max = null;
|
||||||
|
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||||
|
max = max(max, interval.to());
|
||||||
|
}
|
||||||
|
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||||
|
max = max(max, interval.to());
|
||||||
|
}
|
||||||
|
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||||
|
max = max(max, supportEvent.occurredAt());
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSourceKind(TachographFileSession session, TachographTimelineEventBundle bundle) {
|
||||||
|
for (EventHubEventDto event : bundle.allEvents()) {
|
||||||
|
JsonNode raw = raw(event);
|
||||||
|
String sourceKind = text(raw, "sourceKind");
|
||||||
|
if (sourceKind != null) {
|
||||||
|
return sourceKind;
|
||||||
|
}
|
||||||
|
if (event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||||
|
&& event.packageInfo().eventSource().sourceKind() != null) {
|
||||||
|
return event.packageInfo().eventSource().sourceKind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||||
|
if (left == null) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (right == null) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return left.isBefore(right) ? left : right;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||||
|
if (left == null) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (right == null) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return left.isAfter(right) ? left : right;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode raw(EventHubEventDto event) {
|
||||||
|
JsonNode payload = event.payload();
|
||||||
|
if (payload == null || payload.isMissingNode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode raw = payload.get("raw");
|
||||||
|
return raw == null || raw.isNull() ? payload : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
return value == null || value.isNull() ? null : value.asText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean booleanValue(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
return value != null && !value.isNull() && value.asBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long longValue(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
if (value == null || value.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> stringList(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
if (value == null || value.isNull() || !value.isArray()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
value.forEach(item -> {
|
||||||
|
String text = item == null || item.isNull() ? null : item.asText(null);
|
||||||
|
if (text != null && !text.isBlank()) {
|
||||||
|
result.add(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detailText(EventHubEventDto event, String field) {
|
||||||
|
if (event.eventDetails() == null || event.eventDetails().attributes() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = event.eventDetails().attributes().get(field);
|
||||||
|
return value == null || value.isNull() ? null : value.asText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String activityType(EventHubEventDto event) {
|
||||||
|
return switch (event.eventType()) {
|
||||||
|
case DRIVE -> "DRIVE";
|
||||||
|
case WORK -> "WORK";
|
||||||
|
case AVAILABILITY -> "AVAILABILITY";
|
||||||
|
case BREAK_REST -> "BREAK_REST";
|
||||||
|
default -> "UNKNOWN";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long toKilometers(Long meters) {
|
||||||
|
return meters == null ? null : meters / 1_000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ActivityAccumulator {
|
||||||
|
private final String intervalId;
|
||||||
|
private OffsetDateTime startedAt;
|
||||||
|
private OffsetDateTime endedAt;
|
||||||
|
private EventHubEventDto sample;
|
||||||
|
private JsonNode raw;
|
||||||
|
|
||||||
|
private ActivityAccumulator(String intervalId) {
|
||||||
|
this.intervalId = intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void accept(EventHubEventDto event, JsonNode raw) {
|
||||||
|
if (sample == null) {
|
||||||
|
sample = event;
|
||||||
|
this.raw = raw;
|
||||||
|
}
|
||||||
|
if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.START) {
|
||||||
|
startedAt = event.occurredAt();
|
||||||
|
} else if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.END) {
|
||||||
|
endedAt = event.occurredAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResolvedActivityInterval finish() {
|
||||||
|
if (sample == null || startedAt == null || endedAt == null || !endedAt.isAfter(startedAt)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new ResolvedActivityInterval(
|
||||||
|
intervalId,
|
||||||
|
startedAt,
|
||||||
|
endedAt,
|
||||||
|
java.time.Duration.between(startedAt, endedAt).getSeconds(),
|
||||||
|
activityType(sample),
|
||||||
|
detailText(sample, "cardSlot"),
|
||||||
|
detailText(sample, "cardStatus"),
|
||||||
|
detailText(sample, "drivingStatus"),
|
||||||
|
text(raw, "registrationKey"),
|
||||||
|
text(raw, "vehicleKey"),
|
||||||
|
text(raw, "sourceKind"),
|
||||||
|
stringList(raw, "sourceRowIds"),
|
||||||
|
booleanValue(raw, "synthetic"),
|
||||||
|
booleanValue(raw, "clippedToRequestedPeriod"),
|
||||||
|
text(raw, "level") == null ? "RAW_INTERVAL" : text(raw, "level")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VehicleUsageAccumulator {
|
||||||
|
private final UUID sessionId;
|
||||||
|
private final String driverKey;
|
||||||
|
private final String intervalId;
|
||||||
|
private OffsetDateTime startedAt;
|
||||||
|
private OffsetDateTime endedAt;
|
||||||
|
private Long odometerBeginKm;
|
||||||
|
private Long odometerEndKm;
|
||||||
|
private JsonNode raw;
|
||||||
|
|
||||||
|
private VehicleUsageAccumulator(UUID sessionId, String driverKey, String intervalId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.driverKey = driverKey;
|
||||||
|
this.intervalId = intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void accept(EventHubEventDto event, JsonNode raw) {
|
||||||
|
if (this.raw == null) {
|
||||||
|
this.raw = raw;
|
||||||
|
}
|
||||||
|
if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_INSERTED) {
|
||||||
|
startedAt = event.occurredAt();
|
||||||
|
odometerBeginKm = toKilometers(event.odometerM());
|
||||||
|
} else if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_WITHDRAWN) {
|
||||||
|
endedAt = event.occurredAt();
|
||||||
|
odometerEndKm = toKilometers(event.odometerM());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResolvedVehicleUsageInterval finish() {
|
||||||
|
if (startedAt == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ResolvedVehicleUsageInterval.resolved(
|
||||||
|
sessionId,
|
||||||
|
driverKey,
|
||||||
|
intervalId,
|
||||||
|
startedAt,
|
||||||
|
endedAt,
|
||||||
|
odometerBeginKm,
|
||||||
|
odometerEndKm,
|
||||||
|
text(raw, "registrationKey"),
|
||||||
|
text(raw, "vehicleKey"),
|
||||||
|
text(raw, "sourceKind"),
|
||||||
|
stringList(raw, "sourceRowIds")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,17 +35,20 @@ public class TachographFileSessionProcessingService {
|
||||||
private final TachographFileSessionRepository repository;
|
private final TachographFileSessionRepository repository;
|
||||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||||
|
private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder;
|
||||||
private final EventHubProperties properties;
|
private final EventHubProperties properties;
|
||||||
|
|
||||||
public TachographFileSessionProcessingService(
|
public TachographFileSessionProcessingService(
|
||||||
TachographFileSessionRepository repository,
|
TachographFileSessionRepository repository,
|
||||||
DriverTimelineBuilder driverTimelineBuilder,
|
DriverTimelineBuilder driverTimelineBuilder,
|
||||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||||
|
EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
|
||||||
EventHubProperties properties
|
EventHubProperties properties
|
||||||
) {
|
) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||||
|
this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +67,7 @@ public class TachographFileSessionProcessingService {
|
||||||
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
|
ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
|
||||||
OffsetDateTime loadedFrom = timeline.loadedFrom();
|
OffsetDateTime loadedFrom = timeline.loadedFrom();
|
||||||
OffsetDateTime loadedTo = timeline.loadedTo();
|
OffsetDateTime loadedTo = timeline.loadedTo();
|
||||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? loadedFrom : utc(effectiveRequest.occurredFrom());
|
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? loadedFrom : utc(effectiveRequest.occurredFrom());
|
||||||
|
|
@ -146,7 +149,7 @@ public class TachographFileSessionProcessingService {
|
||||||
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
|
ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
|
||||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
|
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
|
||||||
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
|
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
|
||||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||||
|
|
@ -245,6 +248,17 @@ public class TachographFileSessionProcessingService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResolvedDriverTimeline resolveTimeline(
|
||||||
|
TachographFileSession session,
|
||||||
|
DriverExtractionSession driver
|
||||||
|
) {
|
||||||
|
if (properties.getTachographFileSession().getProcessing().getTimelineInputMode()
|
||||||
|
== EventHubProperties.TimelineInputMode.EVENTS) {
|
||||||
|
return eventBackedDriverTimelineBuilder.build(session, driver);
|
||||||
|
}
|
||||||
|
return driverTimelineBuilder.build(session, driver);
|
||||||
|
}
|
||||||
|
|
||||||
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
|
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
|
||||||
List<TachographEsperActivityIntervalEvent> intervals,
|
List<TachographEsperActivityIntervalEvent> intervals,
|
||||||
OffsetDateTime requestedFrom,
|
OffsetDateTime requestedFrom,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.service.EventDetailsFactory;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class EventBackedDriverTimelineBuilderTest {
|
||||||
|
|
||||||
|
private final DriverTimelineBuilder directBuilder = new DriverTimelineBuilder();
|
||||||
|
private final EventBackedDriverTimelineBuilder eventBackedBuilder =
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
directBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconstructsTimelineFromEvents() {
|
||||||
|
DriverExtractionSession driver = new DriverExtractionSession(
|
||||||
|
"12:123",
|
||||||
|
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
|
||||||
|
new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null),
|
||||||
|
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
|
||||||
|
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
|
||||||
|
List.of(new ExtractedCardVehicleUsageInterval(
|
||||||
|
"CVU-1",
|
||||||
|
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||||
|
100L,
|
||||||
|
200L,
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"vu-1"
|
||||||
|
)),
|
||||||
|
List.of(new ExtractedCardActivityInterval(
|
||||||
|
"ACT-1",
|
||||||
|
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"SINGLE",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"a"
|
||||||
|
)),
|
||||||
|
List.of(new ExtractedSupportEvent(
|
||||||
|
"SUP-1",
|
||||||
|
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||||
|
"POSITION",
|
||||||
|
"GNSS_ACCUMULATED_DRIVING",
|
||||||
|
"DRIVER",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
BigDecimal.valueOf(48.2082),
|
||||||
|
BigDecimal.valueOf(16.3738),
|
||||||
|
"AUTHENTIC",
|
||||||
|
150L,
|
||||||
|
null,
|
||||||
|
"raw-path"
|
||||||
|
)),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
TachographFileSession session = new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null),
|
||||||
|
Map.of(driver.driverKey(), driver),
|
||||||
|
new ExtractionStats(1, 1, 1, 1, 1, 0),
|
||||||
|
List.of(),
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||||
|
);
|
||||||
|
|
||||||
|
ResolvedDriverTimeline direct = directBuilder.build(session, driver);
|
||||||
|
ResolvedDriverTimeline reconstructed = eventBackedBuilder.build(session, driver);
|
||||||
|
|
||||||
|
assertThat(reconstructed.sourceKind()).isEqualTo(direct.sourceKind());
|
||||||
|
assertThat(reconstructed.loadedFrom()).isEqualTo(direct.loadedFrom());
|
||||||
|
assertThat(reconstructed.loadedTo()).isEqualTo(direct.loadedTo());
|
||||||
|
assertThat(reconstructed.activityIntervals()).containsExactlyElementsOf(direct.activityIntervals());
|
||||||
|
assertThat(reconstructed.vehicleUsageIntervals()).containsExactlyElementsOf(direct.vehicleUsageIntervals());
|
||||||
|
assertThat(reconstructed.supportEvents()).containsExactlyElementsOf(direct.supportEvents());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import at.procon.eventhub.config.EventHubProperties;
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.service.EventDetailsFactory;
|
||||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
|
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
|
||||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||||
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
|
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
|
||||||
|
|
@ -19,6 +20,7 @@ import java.time.temporal.ChronoUnit;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class TachographFileSessionProcessingServiceTest {
|
class TachographFileSessionProcessingServiceTest {
|
||||||
|
|
@ -32,6 +34,14 @@ class TachographFileSessionProcessingServiceTest {
|
||||||
repository,
|
repository,
|
||||||
driverTimelineBuilder,
|
driverTimelineBuilder,
|
||||||
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
),
|
||||||
properties
|
properties
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -106,6 +116,14 @@ class TachographFileSessionProcessingServiceTest {
|
||||||
repository,
|
repository,
|
||||||
driverTimelineBuilder,
|
driverTimelineBuilder,
|
||||||
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
),
|
||||||
properties
|
properties
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -191,6 +209,92 @@ class TachographFileSessionProcessingServiceTest {
|
||||||
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
|
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canUseEventBackedTimelineModeForEsperProcessing() {
|
||||||
|
EventHubProperties properties = new EventHubProperties();
|
||||||
|
properties.getTachographFileSession().getProcessing().setTimelineInputMode(EventHubProperties.TimelineInputMode.EVENTS);
|
||||||
|
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||||
|
DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder();
|
||||||
|
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
|
||||||
|
repository,
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
),
|
||||||
|
properties
|
||||||
|
);
|
||||||
|
|
||||||
|
DriverExtractionSession driver = new DriverExtractionSession(
|
||||||
|
"12:123",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(
|
||||||
|
new ExtractedCardVehicleUsageInterval(
|
||||||
|
"CVU-1",
|
||||||
|
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-05-01T11:00:00Z"),
|
||||||
|
100L,
|
||||||
|
200L,
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"vu-1"
|
||||||
|
),
|
||||||
|
new ExtractedCardVehicleUsageInterval(
|
||||||
|
"CVU-2",
|
||||||
|
OffsetDateTime.parse("2026-05-01T12:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-05-01T13:00:00Z"),
|
||||||
|
201L,
|
||||||
|
260L,
|
||||||
|
"12:REG-2",
|
||||||
|
"VIN-2",
|
||||||
|
"vu-2"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
List.of(
|
||||||
|
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:30:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
|
||||||
|
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-01T09:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "WORK", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b"),
|
||||||
|
new ExtractedCardActivityInterval("ACT-3", OffsetDateTime.parse("2026-05-01T10:00:00Z"), OffsetDateTime.parse("2026-05-01T10:05:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "c")
|
||||||
|
),
|
||||||
|
List.of(),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
TachographFileSession session = new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
|
||||||
|
Map.of(driver.driverKey(), driver),
|
||||||
|
new ExtractionStats(1, 2, 2, 1, 1, 0),
|
||||||
|
List.of(),
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||||
|
);
|
||||||
|
repository.save(session);
|
||||||
|
|
||||||
|
TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults(
|
||||||
|
session.sessionId(),
|
||||||
|
driver.driverKey(),
|
||||||
|
new TachographEsperEventsProcessingRequest(
|
||||||
|
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-05-01T12:30:00Z"),
|
||||||
|
3,
|
||||||
|
720
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(result.activityIntervalCount()).isEqualTo(3);
|
||||||
|
assertThat(result.drivingIntervalCount()).isEqualTo(2);
|
||||||
|
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
|
||||||
|
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
|
||||||
|
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() {
|
void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() {
|
||||||
EventHubProperties properties = new EventHubProperties();
|
EventHubProperties properties = new EventHubProperties();
|
||||||
|
|
@ -200,6 +304,14 @@ class TachographFileSessionProcessingServiceTest {
|
||||||
repository,
|
repository,
|
||||||
driverTimelineBuilder,
|
driverTimelineBuilder,
|
||||||
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
),
|
||||||
properties
|
properties
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -283,6 +395,14 @@ class TachographFileSessionProcessingServiceTest {
|
||||||
repository,
|
repository,
|
||||||
driverTimelineBuilder,
|
driverTimelineBuilder,
|
||||||
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||||
|
new EventBackedDriverTimelineBuilder(
|
||||||
|
new IntervalBackedDriverTimelineEventBuilder(
|
||||||
|
driverTimelineBuilder,
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory(),
|
||||||
|
new EventDetailsFactory(new ObjectMapper())
|
||||||
|
)
|
||||||
|
),
|
||||||
properties
|
properties
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue