Refine rest candidate derivation chain

This commit is contained in:
trifonovt 2026-05-13 15:22:35 +02:00
parent 3b2f893246
commit 317983eba8
8 changed files with 196 additions and 79 deletions

View File

@ -21,6 +21,7 @@ public record TachographEsperDriverProcessingResultDto(
int drivingIntervalCount, int drivingIntervalCount,
int drivingInterruptionIntervalCount, int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount, int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int potentialHomeOvernightStayIntervalCount, int potentialHomeOvernightStayIntervalCount,
int vehicleUsageIntervalCount, int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount, int vuCardAbsentIntervalCount,
@ -28,6 +29,7 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperActivityIntervalEvent> drivingIntervals, List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals, List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals, List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals, List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals, List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals, List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,

View File

@ -55,6 +55,8 @@ public class DriverTimelineBuilder {
loadResource("esper/tachograph-driving-interruption-interval-events.epl"); loadResource("esper/tachograph-driving-interruption-interval-events.epl");
private static final String DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL = private static final String DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-driving-interruption-vehicle-change-interval-events.epl"); loadResource("esper/tachograph-driving-interruption-vehicle-change-interval-events.epl");
private static final String DAILY_WEEKLY_REST_CANDIDATE_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-daily-weekly-rest-candidate-interval-events.epl");
private static final String POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE = private static final String POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-potential-home-overnight-stay-interval-events.epl"); loadResource("esper/tachograph-potential-home-overnight-stay-interval-events.epl");
private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL = private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL =
@ -209,12 +211,11 @@ public class DriverTimelineBuilder {
} }
public List<TachographEsperPotentialHomeOvernightStayIntervalEvent> buildEsperPotentialHomeOvernightStayIntervalEvents( public List<TachographEsperPotentialHomeOvernightStayIntervalEvent> buildEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals, List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals, List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals
int minimumRestPeriodMinutes
) { ) {
if (drivingInterruptionIntervals == null if (drivingInterruptionVehicleChangeIntervals == null
|| drivingInterruptionIntervals.isEmpty() || drivingInterruptionVehicleChangeIntervals.isEmpty()
|| vuCardAbsentIntervals == null || vuCardAbsentIntervals == null
|| vuCardAbsentIntervals.isEmpty()) { || vuCardAbsentIntervals.isEmpty()) {
return List.of(); return List.of();
@ -223,7 +224,7 @@ public class DriverTimelineBuilder {
executeWithRuntime( executeWithRuntime(
configuration -> { configuration -> {
configuration.getCommon().addEventType( configuration.getCommon().addEventType(
"TachographDrivingInterruptionIntervalInputEvent", "TachographDrivingInterruptionVehicleChangeIntervalInputEvent",
drivingInterruptionIntervalInputDefinition() drivingInterruptionIntervalInputDefinition()
); );
configuration.getCommon().addEventType( configuration.getCommon().addEventType(
@ -231,7 +232,7 @@ public class DriverTimelineBuilder {
vuCardAbsentIntervalInputDefinition() vuCardAbsentIntervalInputDefinition()
); );
}, },
renderPotentialHomeOvernightStayIntervalEventsEpl(minimumRestPeriodMinutes), POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE,
"potentialHomeOvernightStayIntervals", "potentialHomeOvernightStayIntervals",
newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, result), newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, result),
runtime -> { runtime -> {
@ -241,6 +242,34 @@ public class DriverTimelineBuilder {
"TachographVuCardAbsentIntervalInputEvent" "TachographVuCardAbsentIntervalInputEvent"
); );
} }
for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionVehicleChangeIntervals) {
runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval),
"TachographDrivingInterruptionVehicleChangeIntervalInputEvent"
);
}
}
);
return result;
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDailyWeeklyRestCandidateIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
int minimumRestPeriodMinutes
) {
if (drivingInterruptionIntervals == null || drivingInterruptionIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographDrivingInterruptionIntervalInputEvent",
drivingInterruptionIntervalInputDefinition()
),
renderDailyWeeklyRestCandidateIntervalEventsEpl(minimumRestPeriodMinutes),
"dailyWeeklyRestCandidateIntervals",
newData -> collectDrivingInterruptionIntervalEventsFromTimestamps(newData, result),
runtime -> {
for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) { for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) {
runtime.getEventService().sendEventMap( runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval), toDrivingInterruptionIntervalInputMap(interval),
@ -1002,10 +1031,10 @@ public class DriverTimelineBuilder {
); );
} }
private String renderPotentialHomeOvernightStayIntervalEventsEpl(int minimumRestPeriodMinutes) { private String renderDailyWeeklyRestCandidateIntervalEventsEpl(int minimumRestPeriodMinutes) {
long thresholdSeconds = Math.max(1, minimumRestPeriodMinutes) * 60L; long thresholdSeconds = Math.max(1, minimumRestPeriodMinutes) * 60L;
return POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE.replace( return DAILY_WEEKLY_REST_CANDIDATE_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}", "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}",
Long.toString(thresholdSeconds) Long.toString(thresholdSeconds)
); );
} }

View File

@ -174,11 +174,24 @@ public class TachographFileSessionProcessingService {
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(
rawDrivingInterruptionIntervals,
minimumRestPeriodMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDailyWeeklyRestCandidateIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDailyWeeklyRestCandidateIntervals
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents( clipEsperDrivingInterruptionIntervalEvents(
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents( rawDrivingInterruptionVehicleChangeIntervals,
rawDrivingInterruptionIntervals
),
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
@ -187,9 +200,8 @@ public class TachographFileSessionProcessingService {
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals = List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents( clipEsperPotentialHomeOvernightStayIntervalEvents(
driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents( driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
rawDrivingInterruptionIntervals, rawDrivingInterruptionVehicleChangeIntervals,
rawVuCardAbsentIntervals, rawVuCardAbsentIntervals
minimumRestPeriodMinutes
), ),
rawVuCardAbsentIntervals, rawVuCardAbsentIntervals,
requestedFrom, requestedFrom,
@ -218,6 +230,7 @@ public class TachographFileSessionProcessingService {
drivingIntervals.size(), drivingIntervals.size(),
drivingInterruptionIntervals.size(), drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(), drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
potentialHomeOvernightStayIntervals.size(), potentialHomeOvernightStayIntervals.size(),
vehicleUsageIntervals.size(), vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(), vuCardAbsentIntervals.size(),
@ -225,6 +238,7 @@ public class TachographFileSessionProcessingService {
drivingIntervals, drivingIntervals,
drivingInterruptionIntervals, drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals, drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
potentialHomeOvernightStayIntervals, potentialHomeOvernightStayIntervals,
vehicleUsageIntervals, vehicleUsageIntervals,
vuCardAbsentIntervals, vuCardAbsentIntervals,
@ -403,7 +417,12 @@ public class TachographFileSessionProcessingService {
return null; return null;
} }
long durationSeconds = Duration.between(start, end).getSeconds(); long durationSeconds = Duration.between(start, end).getSeconds();
long unknownDurationSeconds = overlapSeconds(start, end, rawVuCardAbsentIntervals, interval.driverKey()); long unknownDurationSeconds = overlapSeconds(
start,
end,
rawVuCardAbsentIntervals,
interval.driverKey()
);
double unknownCoveragePercent = durationSeconds == 0L double unknownCoveragePercent = durationSeconds == 0L
? 0.0d ? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds; : (unknownDurationSeconds * 100.0d) / durationSeconds;
@ -993,8 +1012,9 @@ public class TachographFileSessionProcessingService {
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.", "This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.", "Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.", "Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are DTI intervals where previousRegistrationKey differs from nextRegistrationKey.", "Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"Potential home overnight stay intervals are DTI intervals longer than the configured minimum rest-period threshold where VU card-absent overlap covers at least 95% of the DTI.", "Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidates where VU card-absent overlap covers at least 95% of the candidate interval.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.", "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.", "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." "Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."

View File

@ -0,0 +1,3 @@
@name('dailyWeeklyRestCandidateIntervals')
select *
from TachographDrivingInterruptionIntervalInputEvent(durationSeconds > ${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS});

View File

@ -1,65 +1,65 @@
@name('potentialHomeOvernightStayIntervals') @name('potentialHomeOvernightStayIntervals')
select select
d.sessionId as sessionId, c.sessionId as sessionId,
d.driverKey as driverKey, c.driverKey as driverKey,
d.startedAt as startedAt, c.startedAt as startedAt,
d.endedAt as endedAt, c.endedAt as endedAt,
d.durationSeconds as durationSeconds, c.durationSeconds as durationSeconds,
sum( sum(
case case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.durationSeconds then c.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond when u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond
end end
) as unknownDurationSeconds, ) as unknownDurationSeconds,
(sum( (sum(
case case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.durationSeconds then c.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond when u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond
end end
) * 100.0d) / d.durationSeconds as unknownCoveragePercent, ) * 100.0d) / c.durationSeconds as unknownCoveragePercent,
d.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId,
d.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId,
d.previousRegistrationKey as previousRegistrationKey, c.previousRegistrationKey as previousRegistrationKey,
d.nextRegistrationKey as nextRegistrationKey, c.nextRegistrationKey as nextRegistrationKey,
d.previousVehicleKey as previousVehicleKey, c.previousVehicleKey as previousVehicleKey,
d.nextVehicleKey as nextVehicleKey c.nextVehicleKey as nextVehicleKey
from TachographDrivingInterruptionIntervalInputEvent(durationSeconds > ${POTENTIAL_HOME_OVERNIGHT_STAY_THRESHOLD_SECONDS}) as d unidirectional, from TachographDrivingInterruptionVehicleChangeIntervalInputEvent as c unidirectional,
TachographVuCardAbsentIntervalInputEvent#keepall as u TachographVuCardAbsentIntervalInputEvent#keepall as u
where u.driverKey = d.driverKey where u.driverKey = c.driverKey
and u.startedAtEpochSecond < d.endedAtEpochSecond and u.startedAtEpochSecond < c.endedAtEpochSecond
and u.endedAtEpochSecond > d.startedAtEpochSecond and u.endedAtEpochSecond > c.startedAtEpochSecond
group by group by
d.sessionId, c.sessionId,
d.driverKey, c.driverKey,
d.startedAt, c.startedAt,
d.endedAt, c.endedAt,
d.startedAtEpochSecond, c.startedAtEpochSecond,
d.endedAtEpochSecond, c.endedAtEpochSecond,
d.durationSeconds, c.durationSeconds,
d.previousDrivingSourceIntervalId, c.previousDrivingSourceIntervalId,
d.nextDrivingSourceIntervalId, c.nextDrivingSourceIntervalId,
d.previousRegistrationKey, c.previousRegistrationKey,
d.nextRegistrationKey, c.nextRegistrationKey,
d.previousVehicleKey, c.previousVehicleKey,
d.nextVehicleKey c.nextVehicleKey
having sum( having sum(
case case
when u.startedAtEpochSecond <= d.startedAtEpochSecond and u.endedAtEpochSecond >= d.endedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.durationSeconds then c.durationSeconds
when u.startedAtEpochSecond <= d.startedAtEpochSecond when u.startedAtEpochSecond <= c.startedAtEpochSecond
then u.endedAtEpochSecond - d.startedAtEpochSecond then u.endedAtEpochSecond - c.startedAtEpochSecond
when u.endedAtEpochSecond >= d.endedAtEpochSecond when u.endedAtEpochSecond >= c.endedAtEpochSecond
then d.endedAtEpochSecond - u.startedAtEpochSecond then c.endedAtEpochSecond - u.startedAtEpochSecond
else u.endedAtEpochSecond - u.startedAtEpochSecond else u.endedAtEpochSecond - u.startedAtEpochSecond
end end
) * 100L >= d.durationSeconds * 95L; ) * 100L >= c.durationSeconds * 95L;

View File

@ -84,6 +84,7 @@ class TachographFileSessionControllerTest {
1, 1,
1, 1,
1, 1,
1,
2, 2,
1, 1,
List.of(new TachographEsperActivityIntervalEvent( List.of(new TachographEsperActivityIntervalEvent(
@ -150,6 +151,19 @@ class TachographFileSessionControllerTest {
"VIN-1", "VIN-1",
"VIN-2" "VIN-2"
)), )),
List.of(new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T22:00:00Z"),
43_200L,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent( List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
sessionId, sessionId,
"12:123", "12:123",
@ -258,6 +272,7 @@ class TachographFileSessionControllerTest {
.andExpect(jsonPath("$.activityIntervalCount").value(2)) .andExpect(jsonPath("$.activityIntervalCount").value(2))
.andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1)) .andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1)) .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1))
.andExpect(jsonPath("$.dailyWeeklyRestCandidateIntervalCount").value(1))
.andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1)) .andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1"))

View File

@ -378,6 +378,46 @@ class DriverTimelineBuilderTest {
assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2"); assertThat(absentIntervals.get(0).nextVehicleKey()).isEqualTo("VIN-2");
} }
@Test
void buildsDailyWeeklyRestCandidateIntervalsFromDrivingInterruptionIntervals() {
UUID sessionId = UUID.randomUUID();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = List.of(
new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-01T20:00:00Z"),
36_000L,
"ACT-1",
"ACT-2",
"12:REG-1",
"12:REG-1",
"VIN-1",
"VIN-1"
),
new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-02T10:00:00Z"),
OffsetDateTime.parse("2026-05-02T18:00:00Z"),
28_800L,
"ACT-3",
"ACT-4",
"12:REG-1",
"12:REG-1",
"VIN-1",
"VIN-1"
)
);
List<TachographEsperDrivingInterruptionIntervalEvent> candidates =
builder.buildEsperDailyWeeklyRestCandidateIntervalEvents(drivingInterruptionIntervals, 540);
assertThat(candidates).hasSize(1);
assertThat(candidates.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(candidates.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T20:00:00Z"));
}
@Test @Test
void buildsEsperDrivingInterruptionIntervalEventsFromSignificantDrivingGaps() { void buildsEsperDrivingInterruptionIntervalEventsFromSignificantDrivingGaps() {
DriverExtractionSession driver = new DriverExtractionSession( DriverExtractionSession driver = new DriverExtractionSession(
@ -478,9 +518,9 @@ class DriverTimelineBuilderTest {
"ACT-1", "ACT-1",
"ACT-2", "ACT-2",
"12:REG-1", "12:REG-1",
"12:REG-1", "12:REG-2",
"VIN-1", "VIN-1",
"VIN-1" "VIN-2"
) )
); );
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = List.of( List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = List.of(
@ -499,13 +539,17 @@ class DriverTimelineBuilderTest {
) )
); );
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
builder.buildEsperDailyWeeklyRestCandidateIntervalEvents(interruptions, 720);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
builder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(dailyWeeklyRestCandidateIntervals);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals = List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals =
builder.buildEsperPotentialHomeOvernightStayIntervalEvents( builder.buildEsperPotentialHomeOvernightStayIntervalEvents(
interruptions, drivingInterruptionVehicleChangeIntervals,
vuCardAbsentIntervals, vuCardAbsentIntervals
720
); );
assertThat(drivingInterruptionVehicleChangeIntervals).hasSize(1);
assertThat(intervals).hasSize(1); assertThat(intervals).hasSize(1);
assertThat(intervals.get(0).sessionId()).isEqualTo(sessionId); assertThat(intervals.get(0).sessionId()).isEqualTo(sessionId);
assertThat(intervals.get(0).driverKey()).isEqualTo("12:123"); assertThat(intervals.get(0).driverKey()).isEqualTo("12:123");

View File

@ -87,6 +87,7 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingIntervalCount()).isEqualTo(1); assertThat(result.drivingIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0); assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
@ -169,7 +170,8 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue(); assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue();
assertThat(result.drivingIntervalCount()).isEqualTo(2); assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1); assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z")); 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).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
@ -177,11 +179,6 @@ class TachographFileSessionProcessingServiceTest {
assertThat(result.drivingInterruptionIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2"); assertThat(result.drivingInterruptionIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1"); assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2"); assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-2");
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
assertThat(result.vehicleUsageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z")); assertThat(result.vehicleUsageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.vehicleUsageIntervals().get(0).odometerBeginKm()).isNull(); assertThat(result.vehicleUsageIntervals().get(0).odometerBeginKm()).isNull();
@ -230,7 +227,7 @@ class TachographFileSessionProcessingServiceTest {
), ),
List.of( List.of(
new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"), new ExtractedCardActivityInterval("ACT-1", OffsetDateTime.parse("2026-05-01T08:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "a"),
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:30:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b") new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-02T00:00:00Z"), OffsetDateTime.parse("2026-05-02T00:30:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "b")
), ),
List.of(), List.of(),
List.of() List.of()
@ -257,6 +254,13 @@ class TachographFileSessionProcessingServiceTest {
) )
); );
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(1);
assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.dailyWeeklyRestCandidateIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z"));
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z"));
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(result.drivingInterruptionVehicleChangeIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(1);
assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); 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).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z"));