diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java index 09c05b7..a9ecc28 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -5,6 +5,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeekly import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.OffsetDateTime; @@ -28,6 +29,7 @@ public record TachographEsperDriverProcessingResultDto( int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount, int potentialHomeOvernightStayIntervalCount, int potentialInVehicleOvernightStayIntervalCount, + int potentialInVehicleTripIntervalCount, int vehicleUsageIntervalCount, int vuCardAbsentIntervalCount, List activityIntervals, @@ -39,6 +41,7 @@ public record TachographEsperDriverProcessingResultDto( List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, List potentialHomeOvernightStayIntervals, List potentialInVehicleOvernightStayIntervals, + List potentialInVehicleTripIntervals, List vehicleUsageIntervals, List vuCardAbsentIntervals, List notes diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java index 138df55..2fb2850 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java @@ -10,6 +10,7 @@ public record TachographEsperDrivingDerivedProjectionBundle( List drivingInterruptionVehicleChangeIntervals, List vuCardAbsentIntervals, List potentialHomeOvernightStayIntervals, - List potentialInVehicleOvernightStayIntervals + List potentialInVehicleOvernightStayIntervals, + List potentialInVehicleTripIntervals ) { } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java index 191c1e8..e17266f 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialHomeOvernightStayIntervalEvent.java @@ -9,6 +9,8 @@ public record TachographEsperPotentialHomeOvernightStayIntervalEvent( OffsetDateTime startedAt, OffsetDateTime endedAt, long durationSeconds, + long cardPresentDurationSeconds, + double cardPresentCoveragePercent, long unknownDurationSeconds, double unknownCoveragePercent, String previousDrivingSourceIntervalId, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java index 3355e4d..5d41f5c 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java @@ -11,6 +11,8 @@ public record TachographEsperPotentialInVehicleOvernightStayIntervalEvent( long durationSeconds, long cardPresentDurationSeconds, double cardPresentCoveragePercent, + long unknownDurationSeconds, + double unknownCoveragePercent, String previousDrivingSourceIntervalId, String nextDrivingSourceIntervalId, String previousRegistrationKey, diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java new file mode 100644 index 0000000..122a4c2 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleTripIntervalEvent.java @@ -0,0 +1,25 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public record TachographEsperPotentialInVehicleTripIntervalEvent( + UUID sessionId, + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + String registrationKey, + String vehicleKey, + int containedPotentialInVehicleOvernightStayIntervalCount, + long containedPotentialInVehicleOvernightStayDurationSeconds, + long containedCardPresentDurationSeconds, + long containedUnknownDurationSeconds, + OffsetDateTime firstPotentialInVehicleOvernightStayStartedAt, + OffsetDateTime lastPotentialInVehicleOvernightStayEndedAt, + String firstPreviousDrivingSourceIntervalId, + String lastNextDrivingSourceIntervalId, + List potentialInVehicleOvernightStayIntervals +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java index f8f857b..329e18b 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineBuilder.java @@ -997,6 +997,8 @@ public class DriverTimelineBuilder { (OffsetDateTime) event.get("startedAt"), (OffsetDateTime) event.get("endedAt"), (Long) event.get("durationSeconds"), + 0L, + 0.0d, (Long) event.get("unknownDurationSeconds"), (Double) event.get("unknownCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java index 841e35b..59de3fe 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -19,6 +19,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDeri import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import java.io.IOException; @@ -108,6 +109,7 @@ public class DriverTimelineReusableProjectionBuilder { List vuCardAbsentIntervals = new ArrayList<>(); List potentialHomeOvernightStayIntervals = new ArrayList<>(); List potentialInVehicleOvernightStayIntervals = new ArrayList<>(); + List potentialInVehicleTripIntervals = new ArrayList<>(); executeWithRuntime( configuration -> { @@ -129,7 +131,8 @@ public class DriverTimelineReusableProjectionBuilder { "drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals), "vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals), "potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals), - "potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals) + "potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals), + "potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals) ), runtime -> { if (vehicleUsageIntervals != null) { @@ -159,7 +162,8 @@ public class DriverTimelineReusableProjectionBuilder { sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), sortVuCardAbsentIntervals(vuCardAbsentIntervals), sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals), - sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals) + sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals), + sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals) ); } @@ -172,6 +176,7 @@ public class DriverTimelineReusableProjectionBuilder { List.of(), List.of(), List.of(), + List.of(), List.of() ); } @@ -424,6 +429,8 @@ public class DriverTimelineReusableProjectionBuilder { OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), (Long) event.get("durationSeconds"), + (Long) event.get("cardPresentDurationSeconds"), + (Double) event.get("cardPresentCoveragePercent"), (Long) event.get("unknownDurationSeconds"), (Double) event.get("unknownCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), @@ -454,6 +461,8 @@ public class DriverTimelineReusableProjectionBuilder { (Long) event.get("durationSeconds"), (Long) event.get("cardPresentDurationSeconds"), (Double) event.get("cardPresentCoveragePercent"), + (Long) event.get("unknownDurationSeconds"), + (Double) event.get("unknownCoveragePercent"), (String) event.get("previousDrivingSourceIntervalId"), (String) event.get("nextDrivingSourceIntervalId"), (String) event.get("previousRegistrationKey"), @@ -464,6 +473,37 @@ public class DriverTimelineReusableProjectionBuilder { } } + private void collectPotentialInVehicleTripIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperPotentialInVehicleTripIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), + (Long) event.get("durationSeconds"), + (String) event.get("registrationKey"), + (String) event.get("vehicleKey"), + (Integer) event.get("containedPotentialInVehicleOvernightStayIntervalCount"), + (Long) event.get("containedPotentialInVehicleOvernightStayDurationSeconds"), + (Long) event.get("containedCardPresentDurationSeconds"), + (Long) event.get("containedUnknownDurationSeconds"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("firstPotentialInVehicleOvernightStayStartedAtEpochSecond")), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("lastPotentialInVehicleOvernightStayEndedAtEpochSecond")), ZoneOffset.UTC), + (String) event.get("firstPreviousDrivingSourceIntervalId"), + (String) event.get("lastNextDrivingSourceIntervalId"), + List.of() + )); + } + } + private List sortDrivingInterruptionIntervals( List intervals ) { @@ -509,6 +549,15 @@ public class DriverTimelineReusableProjectionBuilder { .toList(); } + private List sortPotentialInVehicleTripIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt)) + .toList(); + } + private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE .replace( diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 58c5bf8..456788d 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -19,6 +19,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInte import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.Duration; @@ -202,17 +203,18 @@ public class TachographFileSessionProcessingService { requestedFrom, requestedTo ); + List rawVehicleUsageIntervals = + driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline); List rawVuCardAbsentIntervals = derivedProjectionBundle.vuCardAbsentIntervals(); List potentialHomeOvernightStayIntervals = clipEsperPotentialHomeOvernightStayIntervalEvents( derivedProjectionBundle.potentialHomeOvernightStayIntervals(), rawVuCardAbsentIntervals, + rawVehicleUsageIntervals, requestedFrom, requestedTo ); - List rawVehicleUsageIntervals = - driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline); List dailyWeeklyRestCandidateCoverageIntervals = clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(), @@ -232,10 +234,18 @@ public class TachographFileSessionProcessingService { List potentialInVehicleOvernightStayIntervals = clipEsperPotentialInVehicleOvernightStayIntervalEvents( derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(), + rawVuCardAbsentIntervals, rawVehicleUsageIntervals, requestedFrom, requestedTo ); + List potentialInVehicleTripIntervals = + clipEsperPotentialInVehicleTripIntervalEvents( + derivedProjectionBundle.potentialInVehicleTripIntervals(), + potentialInVehicleOvernightStayIntervals, + requestedFrom, + requestedTo + ); List vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents( rawVehicleUsageIntervals, requestedFrom, @@ -264,10 +274,11 @@ public class TachographFileSessionProcessingService { unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(), potentialHomeOvernightStayIntervals.size(), potentialInVehicleOvernightStayIntervals.size(), + potentialInVehicleTripIntervals.size(), vehicleUsageIntervals.size(), vuCardAbsentIntervals.size(), - activityIntervals, - drivingIntervals, + List.of()/*activityIntervals*/, + List.of()/*drivingIntervals*/, drivingInterruptionIntervals, drivingInterruptionVehicleChangeIntervals, dailyWeeklyRestCandidateIntervals, @@ -275,6 +286,7 @@ public class TachographFileSessionProcessingService { unclassifiedDailyWeeklyRestCandidateCoverageIntervals, potentialHomeOvernightStayIntervals, potentialInVehicleOvernightStayIntervals, + potentialInVehicleTripIntervals, vehicleUsageIntervals, vuCardAbsentIntervals, esperProjectionNotes() @@ -510,53 +522,6 @@ public class TachographFileSessionProcessingService { private List clipEsperPotentialHomeOvernightStayIntervalEvents( List intervals, List rawVuCardAbsentIntervals, - OffsetDateTime requestedFrom, - OffsetDateTime requestedTo - ) { - if (requestedFrom == null || requestedTo == null) { - return List.of(); - } - return intervals.stream() - .map(interval -> { - OffsetDateTime start = max(interval.startedAt(), requestedFrom); - OffsetDateTime end = min(interval.endedAt(), requestedTo); - if (!end.isAfter(start)) { - return null; - } - long durationSeconds = Duration.between(start, end).getSeconds(); - long unknownDurationSeconds = overlapSeconds( - start, - end, - rawVuCardAbsentIntervals, - interval.driverKey() - ); - double unknownCoveragePercent = durationSeconds == 0L - ? 0.0d - : (unknownDurationSeconds * 100.0d) / durationSeconds; - return new TachographEsperPotentialHomeOvernightStayIntervalEvent( - interval.sessionId(), - interval.driverKey(), - start, - end, - durationSeconds, - unknownDurationSeconds, - unknownCoveragePercent, - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId(), - interval.previousRegistrationKey(), - interval.nextRegistrationKey(), - interval.previousVehicleKey(), - interval.nextVehicleKey() - ); - }) - .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) - .toList(); - } - - private List clipEsperPotentialInVehicleOvernightStayIntervalEvents( - List intervals, List rawVehicleUsageIntervals, OffsetDateTime requestedFrom, OffsetDateTime requestedTo @@ -577,11 +542,81 @@ public class TachographFileSessionProcessingService { end, rawVehicleUsageIntervals, interval.driverKey(), - interval.previousRegistrationKey() + null ); double cardPresentCoveragePercent = durationSeconds == 0L ? 0.0d : (cardPresentDurationSeconds * 100.0d) / durationSeconds; + long unknownDurationSeconds = overlapSeconds( + start, + end, + rawVuCardAbsentIntervals, + interval.driverKey() + ); + double unknownCoveragePercent = durationSeconds == 0L + ? 0.0d + : (unknownDurationSeconds * 100.0d) / durationSeconds; + return new TachographEsperPotentialHomeOvernightStayIntervalEvent( + interval.sessionId(), + interval.driverKey(), + start, + end, + durationSeconds, + cardPresentDurationSeconds, + cardPresentCoveragePercent, + unknownDurationSeconds, + unknownCoveragePercent, + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId(), + interval.previousRegistrationKey(), + interval.nextRegistrationKey(), + interval.previousVehicleKey(), + interval.nextVehicleKey() + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) + .toList(); + } + + private List clipEsperPotentialInVehicleOvernightStayIntervalEvents( + List intervals, + List rawVuCardAbsentIntervals, + List rawVehicleUsageIntervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo + ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } + return intervals.stream() + .map(interval -> { + OffsetDateTime start = max(interval.startedAt(), requestedFrom); + OffsetDateTime end = min(interval.endedAt(), requestedTo); + if (!end.isAfter(start)) { + return null; + } + long durationSeconds = Duration.between(start, end).getSeconds(); + long cardPresentDurationSeconds = overlapVehicleUsageSeconds( + start, + end, + rawVehicleUsageIntervals, + interval.driverKey(), + null + ); + double cardPresentCoveragePercent = durationSeconds == 0L + ? 0.0d + : (cardPresentDurationSeconds * 100.0d) / durationSeconds; + long unknownDurationSeconds = overlapSeconds( + start, + end, + rawVuCardAbsentIntervals, + interval.driverKey() + ); + double unknownCoveragePercent = durationSeconds == 0L + ? 0.0d + : (unknownDurationSeconds * 100.0d) / durationSeconds; return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( interval.sessionId(), interval.driverKey(), @@ -590,6 +625,8 @@ public class TachographFileSessionProcessingService { durationSeconds, cardPresentDurationSeconds, cardPresentCoveragePercent, + unknownDurationSeconds, + unknownCoveragePercent, interval.previousDrivingSourceIntervalId(), interval.nextDrivingSourceIntervalId(), interval.previousRegistrationKey(), @@ -604,6 +641,98 @@ public class TachographFileSessionProcessingService { .toList(); } + private List clipEsperPotentialInVehicleTripIntervalEvents( + List intervals, + List potentialInVehicleOvernightStayIntervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo + ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } + if (intervals == null || intervals.isEmpty() + || potentialInVehicleOvernightStayIntervals == null || potentialInVehicleOvernightStayIntervals.isEmpty()) { + return List.of(); + } + return intervals.stream() + .map(interval -> { + OffsetDateTime start = max(interval.startedAt(), requestedFrom); + OffsetDateTime end = min(interval.endedAt(), requestedTo); + if (!end.isAfter(start)) { + return null; + } + List containedIntervals = + potentialInVehicleOvernightStayIntervals.stream() + .filter(candidate -> tripContainsPotentialInterval( + interval.driverKey(), + interval.registrationKey(), + interval.vehicleKey(), + start, + end, + candidate + )) + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) + .toList(); + if (containedIntervals.isEmpty()) { + return null; + } + TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0); + TachographEsperPotentialInVehicleOvernightStayIntervalEvent last = + containedIntervals.get(containedIntervals.size() - 1); + return new TachographEsperPotentialInVehicleTripIntervalEvent( + interval.sessionId(), + interval.driverKey(), + start, + end, + Duration.between(start, end).getSeconds(), + interval.registrationKey(), + interval.vehicleKey(), + containedIntervals.size(), + containedIntervals.stream() + .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds) + .sum(), + containedIntervals.stream() + .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardPresentDurationSeconds) + .sum(), + containedIntervals.stream() + .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::unknownDurationSeconds) + .sum(), + first.startedAt(), + last.endedAt(), + first.previousDrivingSourceIntervalId(), + last.nextDrivingSourceIntervalId(), + containedIntervals + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt)) + .toList(); + } + + private boolean tripContainsPotentialInterval( + String driverKey, + String registrationKey, + String vehicleKey, + OffsetDateTime tripStartedAt, + OffsetDateTime tripEndedAt, + TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate + ) { + if (!Objects.equals(driverKey, candidate.driverKey())) { + return false; + } + if (!Objects.equals(registrationKey, candidate.previousRegistrationKey())) { + return false; + } + if (vehicleKey != null && candidate.previousVehicleKey() != null + && !Objects.equals(vehicleKey, candidate.previousVehicleKey())) { + return false; + } + return !candidate.startedAt().isBefore(tripStartedAt) + && !candidate.endedAt().isAfter(tripEndedAt); + } + private List synthesizeUnknownGaps( List knownIntervals, Duration gapDetectionTolerance @@ -1174,6 +1303,7 @@ public class TachographFileSessionProcessingService { "Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.", "Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.", "Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.", + "Potential in-vehicle trip intervals span from the end of the coverage interval before a same-vehicle in-vehicle-overnight sequence to the start of the first coverage interval after that sequence.", "VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.", "occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.", "Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval." diff --git a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl index 784276d..6886f35 100644 --- a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl +++ b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl @@ -137,6 +137,8 @@ create schema PotentialHomeOvernightStayInterval( startedAtEpochSecond long, endedAtEpochSecond long, durationSeconds long, + cardPresentDurationSeconds long, + cardPresentCoveragePercent double, unknownDurationSeconds long, unknownCoveragePercent double, previousDrivingSourceIntervalId string, @@ -155,6 +157,8 @@ create schema PotentialInVehicleOvernightStayInterval( durationSeconds long, cardPresentDurationSeconds long, cardPresentCoveragePercent double, + unknownDurationSeconds long, + unknownCoveragePercent double, previousDrivingSourceIntervalId string, nextDrivingSourceIntervalId string, previousRegistrationKey string, @@ -181,6 +185,44 @@ create schema UnclassifiedDailyWeeklyRestCandidateCoverageInterval( nextVehicleKey string ); +create schema PotentialInVehicleTripState( + sessionId java.util.UUID, + driverKey string, + tripStartedAtEpochSecond long, + registrationKey string, + vehicleKey string, + containedPotentialInVehicleOvernightStayIntervalCount int, + containedPotentialInVehicleOvernightStayDurationSeconds long, + containedCardPresentDurationSeconds long, + containedUnknownDurationSeconds long, + firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, + lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, + firstPreviousDrivingSourceIntervalId string, + lastNextDrivingSourceIntervalId string +); + +create schema PotentialInVehicleTripInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + registrationKey string, + vehicleKey string, + containedPotentialInVehicleOvernightStayIntervalCount int, + containedPotentialInVehicleOvernightStayDurationSeconds long, + containedCardPresentDurationSeconds long, + containedUnknownDurationSeconds long, + firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, + lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, + firstPreviousDrivingSourceIntervalId string, + lastNextDrivingSourceIntervalId string +); + +create window PreviousRestCandidateCoverageInterval#unique(driverKey) as DailyWeeklyRestCandidateCoverageInterval; + +create window OpenPotentialInVehicleTripState#unique(driverKey) as PotentialInVehicleTripState; + insert into SignificantDrivingInterval select sessionId, @@ -435,6 +477,8 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, c.durationSeconds as durationSeconds, + c.cardPresentDurationSeconds as cardPresentDurationSeconds, + c.cardPresentCoveragePercent as cardPresentCoveragePercent, c.unknownDurationSeconds as unknownDurationSeconds, c.unknownCoveragePercent as unknownCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, @@ -458,6 +502,8 @@ select c.durationSeconds as durationSeconds, c.cardPresentDurationSeconds as cardPresentDurationSeconds, c.cardPresentCoveragePercent as cardPresentCoveragePercent, + c.unknownDurationSeconds as unknownDurationSeconds, + c.unknownCoveragePercent as unknownCoveragePercent, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.previousRegistrationKey as previousRegistrationKey, @@ -501,6 +547,130 @@ where not ( and c.cardPresentDurationSeconds >= c.durationSeconds ); +@Priority(40) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into PotentialInVehicleTripInterval +select + s.sessionId as sessionId, + s.driverKey as driverKey, + s.tripStartedAtEpochSecond as startedAtEpochSecond, + c.startedAtEpochSecond as endedAtEpochSecond, + c.startedAtEpochSecond - s.tripStartedAtEpochSecond as durationSeconds, + s.registrationKey as registrationKey, + s.vehicleKey as vehicleKey, + s.containedPotentialInVehicleOvernightStayIntervalCount as containedPotentialInVehicleOvernightStayIntervalCount, + s.containedPotentialInVehicleOvernightStayDurationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + s.containedCardPresentDurationSeconds as containedCardPresentDurationSeconds, + s.containedUnknownDurationSeconds as containedUnknownDurationSeconds, + s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + s.lastPotentialInVehicleOvernightStayEndedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + s.lastNextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and c.startedAtEpochSecond > s.tripStartedAtEpochSecond + and ( + not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds + ) + or s.registrationKey != c.previousRegistrationKey + or ( + s.vehicleKey is not null + and c.previousVehicleKey is not null + and s.vehicleKey != c.previousVehicleKey + ) + ); + +@Priority(35) +on DailyWeeklyRestCandidateCoverageInterval as c +delete from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds + ); + +@Priority(30) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into OpenPotentialInVehicleTripState +select + s.sessionId as sessionId, + s.driverKey as driverKey, + s.tripStartedAtEpochSecond as tripStartedAtEpochSecond, + s.registrationKey as registrationKey, + s.vehicleKey as vehicleKey, + s.containedPotentialInVehicleOvernightStayIntervalCount + 1 as containedPotentialInVehicleOvernightStayIntervalCount, + s.containedPotentialInVehicleOvernightStayDurationSeconds + c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + s.containedCardPresentDurationSeconds + c.cardPresentDurationSeconds as containedCardPresentDurationSeconds, + s.containedUnknownDurationSeconds + c.unknownDurationSeconds as containedUnknownDurationSeconds, + s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds + and s.registrationKey = c.previousRegistrationKey + and ( + s.vehicleKey is null + or c.previousVehicleKey is null + or s.vehicleKey = c.previousVehicleKey + ); + +@Priority(20) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into OpenPotentialInVehicleTripState +select + c.sessionId as sessionId, + c.driverKey as driverKey, + priorCoverage.endedAtEpochSecond as tripStartedAtEpochSecond, + c.previousRegistrationKey as registrationKey, + case + when c.previousVehicleKey is not null then c.previousVehicleKey + else c.nextVehicleKey + end as vehicleKey, + 1 as containedPotentialInVehicleOvernightStayIntervalCount, + c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + c.cardPresentDurationSeconds as containedCardPresentDurationSeconds, + c.unknownDurationSeconds as containedUnknownDurationSeconds, + c.startedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + c.previousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from PreviousRestCandidateCoverageInterval as priorCoverage +where priorCoverage.driverKey = c.driverKey + and c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds + and not exists ( + select * from OpenPotentialInVehicleTripState as s + where s.driverKey = c.driverKey + and s.registrationKey = c.previousRegistrationKey + and ( + s.vehicleKey is null + or c.previousVehicleKey is null + or s.vehicleKey = c.previousVehicleKey + ) + ); + +@Priority(10) +on DailyWeeklyRestCandidateCoverageInterval +delete from PreviousRestCandidateCoverageInterval; + +@Priority(5) +on DailyWeeklyRestCandidateCoverageInterval as current +insert into PreviousRestCandidateCoverageInterval +select *; + @name('drivingInterruptionIntervals') select * from DrivingInterruptionInterval; @@ -522,5 +692,8 @@ select * from PotentialHomeOvernightStayInterval; @name('potentialInVehicleOvernightStayIntervals') select * from PotentialInVehicleOvernightStayInterval; +@name('potentialInVehicleTripIntervals') +select * from PotentialInVehicleTripInterval; + @name('unclassifiedDailyWeeklyRestCandidateCoverageIntervals') select * from UnclassifiedDailyWeeklyRestCandidateCoverageInterval; diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index f52fe8c..bd086b5 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -23,6 +23,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityInt import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService; @@ -89,6 +90,7 @@ class TachographFileSessionControllerTest { 0, 1, 0, + 0, 2, 1, List.of(new TachographEsperActivityIntervalEvent( @@ -192,6 +194,8 @@ class TachographFileSessionControllerTest { OffsetDateTime.parse("2026-05-12T10:00:00Z"), OffsetDateTime.parse("2026-05-12T22:00:00Z"), 43_200L, + 0L, + 0.0d, 43_200L, 100.0d, "ACT-2", @@ -202,6 +206,7 @@ class TachographFileSessionControllerTest { "VIN-2" )), List.of(), + List.of(), List.of(new TachographEsperVehicleUsageIntervalEvent( sessionId, "12:123", @@ -300,6 +305,7 @@ class TachographFileSessionControllerTest { .andExpect(jsonPath("$.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount").value(0)) .andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1)) .andExpect(jsonPath("$.potentialInVehicleOvernightStayIntervalCount").value(0)) + .andExpect(jsonPath("$.potentialInVehicleTripIntervalCount").value(0)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2")) diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index ec44e55..3258d79 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -104,6 +104,7 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); assertThat(result.vuCardAbsentIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z")); @@ -201,6 +202,7 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.drivingInterruptionIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z")); assertThat(result.drivingInterruptionIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); assertThat(result.drivingInterruptionIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1"); @@ -300,6 +302,7 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); } @@ -397,8 +400,11 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); assertThat(result.potentialHomeOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z")); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(0L); + assertThat(result.potentialHomeOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(0.0d); assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); } @@ -476,6 +482,7 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(1); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L); @@ -483,6 +490,8 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(0.0d); } @Test @@ -565,6 +574,7 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(0); assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt()) .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); @@ -576,6 +586,80 @@ class TachographFileSessionProcessingServiceTest { .isLessThan(95.0d); } + @Test + void returnsPotentialInVehicleTripIntervalsForSameVehicleOvernightSequenceBoundedByCoverageIntervals() { + EventHubProperties properties = new EventHubProperties(); + 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-0", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), 100L, 150L, "12:REG-0", "VIN-0", "vu-0"), + new ExtractedCardVehicleUsageInterval("CVU-1", OffsetDateTime.parse("2026-05-01T22:00:00Z"), OffsetDateTime.parse("2026-05-04T02:00:00Z"), 200L, 400L, "12:REG-1", "VIN-1", "vu-1"), + new ExtractedCardVehicleUsageInterval("CVU-2", OffsetDateTime.parse("2026-05-05T00:00:00Z"), OffsetDateTime.parse("2026-05-05T02:00:00Z"), 401L, 430L, "12:REG-2", "VIN-2", "vu-2") + ), + List.of( + new ExtractedCardActivityInterval("ACT-0", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T09:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-0", "VIN-0", "a"), + new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T22:00:00Z"), OffsetDateTime.parse("2026-05-02T00:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b"), + new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T20:00:00Z"), OffsetDateTime.parse("2026-05-02T22:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "c"), + new ExtractedCardActivityInterval("ACT-3", OffsetDateTime.parse("2026-05-03T20:00:00Z"), OffsetDateTime.parse("2026-05-03T22:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "d"), + new ExtractedCardActivityInterval("ACT-4", OffsetDateTime.parse("2026-05-05T00:00:00Z"), OffsetDateTime.parse("2026-05-05T02:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "e") + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 5, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 5, 3, 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-01T10:00:00Z"), + OffsetDateTime.parse("2026-05-05T00:00:00Z"), + 3, + 720 + ) + ); + + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(2); + assertThat(result.potentialInVehicleTripIntervalCount()).isEqualTo(1); + assertThat(result.potentialInVehicleTripIntervals().get(0).startedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-01T22:00:00Z")); + assertThat(result.potentialInVehicleTripIntervals().get(0).endedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-03T22:00:00Z")); + assertThat(result.potentialInVehicleTripIntervals().get(0).containedPotentialInVehicleOvernightStayIntervalCount()).isEqualTo(2); + assertThat(result.potentialInVehicleTripIntervals().get(0).potentialInVehicleOvernightStayIntervals()).hasSize(2); + assertThat(result.potentialInVehicleTripIntervals().get(0).registrationKey()).isEqualTo("12:REG-1"); + } + @Test void evaluatesOperatingPeriodsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties();