Add event-backed tachograph timeline mode

This commit is contained in:
trifonovt 2026-05-20 09:03:35 +02:00
parent 9ef8bfc412
commit 2ded38a28a
5 changed files with 689 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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