Compare commits

..

4 Commits

Author SHA1 Message Date
trifonovt 317983eba8 Refine rest candidate derivation chain 2026-05-13 15:22:35 +02:00
trifonovt 3b2f893246 Add rest candidate Esper projections 2026-05-13 13:48:29 +02:00
trifonovt eb4e04f144 Add esper driving interruption projections 2026-05-13 12:35:39 +02:00
trifonovt 0e2b83270c Add tachograph Esper driver event projections 2026-05-13 11:32:16 +02:00
24 changed files with 2534 additions and 2 deletions

View File

@ -351,6 +351,61 @@
}
}
},
{
"name": "Get tachograph file session driver Esper events",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"tachograph-file-sessions",
"{{sessionId}}",
"drivers",
"{{driverKey}}",
"processing",
"esper-events"
]
}
}
},
{
"name": "Process tachograph file session Esper events",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions/{{sessionId}}/drivers/{{driverKey}}/processing/esper-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"tachograph-file-sessions",
"{{sessionId}}",
"drivers",
"{{driverKey}}",
"processing",
"esper-events"
]
}
}
},
{
"name": "Process tachograph file session operating periods",
"request": {

View File

@ -358,6 +358,7 @@ public class EventHubProperties {
public static class Processing {
private int operatingSplitIdleHours = 7;
private int significantDrivingMinutes = 3;
private int minimumRestPeriodMinutes = 720;
private int mergeGapSeconds = 0;
private int gapDetectionToleranceSeconds = 0;
@ -377,6 +378,14 @@ public class EventHubProperties {
this.significantDrivingMinutes = Math.max(1, significantDrivingMinutes);
}
public int getMinimumRestPeriodMinutes() {
return minimumRestPeriodMinutes;
}
public void setMinimumRestPeriodMinutes(int minimumRestPeriodMinutes) {
this.minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes);
}
public int getMergeGapSeconds() {
return mergeGapSeconds;
}

View File

@ -1,6 +1,8 @@
package at.procon.eventhub.tachographfilesession.api;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
@ -66,6 +68,23 @@ public class TachographFileSessionController {
return ResponseEntity.ok(service.getDriver(sessionId, driverKey));
}
@GetMapping("/{sessionId}/drivers/{driverKey}/processing/esper-events")
public ResponseEntity<TachographEsperDriverProcessingResultDto> getEsperDriverProcessingResults(
@PathVariable UUID sessionId,
@PathVariable String driverKey
) {
return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey));
}
@PostMapping("/{sessionId}/drivers/{driverKey}/processing/esper-events")
public ResponseEntity<TachographEsperDriverProcessingResultDto> evaluateEsperDriverProcessingResults(
@PathVariable UUID sessionId,
@PathVariable String driverKey,
@RequestBody(required = false) TachographEsperEventsProcessingRequest request
) {
return ResponseEntity.ok(processingService.getEsperDriverProcessingResults(sessionId, driverKey, request));
}
@PostMapping("/{sessionId}/drivers/{driverKey}/processing/operating-periods")
public ResponseEntity<TachographOperatingPeriodsProcessingResultDto> evaluateOperatingPeriods(
@PathVariable UUID sessionId,

View File

@ -0,0 +1,38 @@
package at.procon.eventhub.tachographfilesession.dto;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record TachographEsperDriverProcessingResultDto(
UUID sessionId,
String driverKey,
String sourceKind,
OffsetDateTime loadedFrom,
OffsetDateTime loadedTo,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo,
int activityIntervalCount,
int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
List<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<String> notes
) {
}

View File

@ -0,0 +1,17 @@
package at.procon.eventhub.tachographfilesession.dto;
import java.time.OffsetDateTime;
public record TachographEsperEventsProcessingRequest(
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
Integer significantDrivingMinutes,
Integer minimumRestPeriodMinutes
) {
public TachographEsperEventsProcessingRequest {
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
minimumRestPeriodMinutes = minimumRestPeriodMinutes == null
? null
: Math.max(1, minimumRestPeriodMinutes);
}
}

View File

@ -3,8 +3,11 @@ package at.procon.eventhub.tachographfilesession.model;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record ResolvedVehicleUsageInterval(
UUID sessionId,
String driverKey,
String intervalId,
OffsetDateTime from,
OffsetDateTime to,
@ -17,6 +20,8 @@ public record ResolvedVehicleUsageInterval(
List<String> sourceIntervalIds
) {
public static ResolvedVehicleUsageInterval resolved(
UUID sessionId,
String driverKey,
String intervalId,
OffsetDateTime from,
OffsetDateTime to,
@ -28,6 +33,8 @@ public record ResolvedVehicleUsageInterval(
List<String> sourceIntervalIds
) {
return new ResolvedVehicleUsageInterval(
sessionId,
driverKey,
intervalId,
from,
to,

View File

@ -0,0 +1,26 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record TachographEsperActivityIntervalEvent(
UUID sessionId,
String driverKey,
String intervalId,
String activityType,
String cardSlot,
String cardStatus,
String drivingStatus,
String registrationKey,
String vehicleKey,
String sourceKind,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
List<String> sourceIntervalIds,
boolean synthetic,
boolean clippedToRequestedPeriod,
String level
) {
}

View File

@ -0,0 +1,19 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperDrivingInterruptionIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -0,0 +1,21 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperPotentialHomeOvernightStayIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
long unknownDurationSeconds,
double unknownCoveragePercent,
String previousDrivingSourceIntervalId,
String nextDrivingSourceIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -0,0 +1,21 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record TachographEsperVehicleUsageIntervalEvent(
UUID sessionId,
String driverKey,
String intervalId,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
Long odometerBeginKm,
Long odometerEndKm,
String registrationKey,
String vehicleKey,
String sourceKind,
List<String> sourceIntervalIds
) {
}

View File

@ -0,0 +1,19 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
import java.util.UUID;
public record TachographEsperVuCardAbsentIntervalEvent(
UUID sessionId,
String driverKey,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long durationSeconds,
String previousUsageIntervalId,
String nextUsageIntervalId,
String previousRegistrationKey,
String nextRegistrationKey,
String previousVehicleKey,
String nextVehicleKey
) {
}

View File

@ -1,5 +1,15 @@
package at.procon.eventhub.tachographfilesession.service;
import com.espertech.esper.common.client.EPCompiled;
import com.espertech.esper.common.client.EventBean;
import com.espertech.esper.common.client.configuration.Configuration;
import com.espertech.esper.compiler.client.CompilerArguments;
import com.espertech.esper.compiler.client.EPCompileException;
import com.espertech.esper.compiler.client.EPCompilerProvider;
import com.espertech.esper.runtime.client.EPDeployException;
import com.espertech.esper.runtime.client.EPDeployment;
import com.espertech.esper.runtime.client.EPRuntime;
import com.espertech.esper.runtime.client.EPRuntimeProvider;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
@ -8,22 +18,60 @@ 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.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import java.time.Duration;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
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 java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
@Component
public class DriverTimelineBuilder {
private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
private static final String ACTIVITY_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-activity-interval-events.epl");
private static final String DRIVING_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-driving-interval-events.epl");
private static final String DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE =
loadResource("esper/tachograph-driving-interruption-interval-events.epl");
private static final String 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 =
loadResource("esper/tachograph-potential-home-overnight-stay-interval-events.epl");
private static final String VEHICLE_USAGE_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-vehicle-usage-interval-events.epl");
private static final String VU_CARD_ABSENT_INTERVAL_EVENTS_EPL =
loadResource("esper/tachograph-vu-card-absent-interval-events.epl");
public ResolvedDriverTimeline build(TachographFileSession session, DriverExtractionSession driverSession) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(driverSession.cardVehicleUsageIntervals(), sourceKind);
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
List<ExtractedSupportEvent> supportEvents = driverSession.supportEvents().stream()
.sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt)
@ -44,7 +92,360 @@ public class DriverTimelineBuilder {
);
}
public List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperActivityIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals);
}
public List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline
) {
return timeline == null
? List.of()
: buildEsperActivityIntervalEvents(sessionId, driverKey, timeline.activityIntervals());
}
public List<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperDrivingIntervalEvents(session.sessionId(), driverSession.driverKey(), activityIntervals);
}
public List<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline
) {
if (timeline == null) {
return List.of();
}
return buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline.activityIntervals());
}
public List<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
return buildEsperVehicleUsageIntervalEvents(vehicleUsageIntervals);
}
public List<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
ResolvedDriverTimeline timeline
) {
return timeline == null ? List.of() : buildEsperVehicleUsageIntervalEvents(timeline.vehicleUsageIntervals());
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession,
int significantDrivingMinutes
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedActivityInterval> activityIntervals = resolveActivities(driverSession.cardActivityIntervals(), sourceKind);
return buildEsperDrivingInterruptionIntervalEvents(
session.sessionId(),
driverSession.driverKey(),
activityIntervals,
significantDrivingMinutes
);
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
UUID sessionId,
String driverKey,
ResolvedDriverTimeline timeline,
int significantDrivingMinutes
) {
if (timeline == null) {
return List.of();
}
return buildEsperDrivingInterruptionIntervalEvents(
sessionId,
driverKey,
timeline.activityIntervals(),
significantDrivingMinutes
);
}
public List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals
) {
if (drivingInterruptionIntervals == null || drivingInterruptionIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographDrivingInterruptionIntervalInputEvent",
drivingInterruptionIntervalInputDefinition()
),
DRIVING_INTERRUPTION_VEHICLE_CHANGE_INTERVAL_EVENTS_EPL,
"drivingInterruptionVehicleChangeIntervals",
newData -> collectDrivingInterruptionIntervalEventsFromTimestamps(newData, result),
runtime -> {
for (TachographEsperDrivingInterruptionIntervalEvent interval : drivingInterruptionIntervals) {
runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval),
"TachographDrivingInterruptionIntervalInputEvent"
);
}
}
);
return result;
}
public List<TachographEsperPotentialHomeOvernightStayIntervalEvent> buildEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals
) {
if (drivingInterruptionVehicleChangeIntervals == null
|| drivingInterruptionVehicleChangeIntervals.isEmpty()
|| vuCardAbsentIntervals == null
|| vuCardAbsentIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> {
configuration.getCommon().addEventType(
"TachographDrivingInterruptionVehicleChangeIntervalInputEvent",
drivingInterruptionIntervalInputDefinition()
);
configuration.getCommon().addEventType(
"TachographVuCardAbsentIntervalInputEvent",
vuCardAbsentIntervalInputDefinition()
);
},
POTENTIAL_HOME_OVERNIGHT_STAY_INTERVAL_EVENTS_EPL_TEMPLATE,
"potentialHomeOvernightStayIntervals",
newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, result),
runtime -> {
for (TachographEsperVuCardAbsentIntervalEvent interval : vuCardAbsentIntervals) {
runtime.getEventService().sendEventMap(
toVuCardAbsentIntervalInputMap(interval),
"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) {
runtime.getEventService().sendEventMap(
toDrivingInterruptionIntervalInputMap(interval),
"TachographDrivingInterruptionIntervalInputEvent"
);
}
}
);
return result;
}
public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(
session.sessionId(),
driverSession.driverKey(),
driverSession.cardVehicleUsageIntervals(),
sourceKind
);
return buildEsperVuCardAbsentIntervalEvents(vehicleUsageIntervals);
}
public List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
ResolvedDriverTimeline timeline
) {
return timeline == null ? List.of() : buildEsperVuCardAbsentIntervalEvents(timeline.vehicleUsageIntervals());
}
private List<TachographEsperActivityIntervalEvent> buildEsperActivityIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals
) {
if (activityIntervals == null || activityIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperActivityIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographActivityIntervalInputEvent",
activityIntervalInputDefinition()
),
ACTIVITY_INTERVAL_EVENTS_EPL,
"activityIntervals",
newData -> collectActivityIntervalEvents(newData, result),
runtime -> {
for (ResolvedActivityInterval interval : activityIntervals) {
runtime.getEventService().sendEventMap(
toActivityIntervalInputMap(sessionId, driverKey, interval),
"TachographActivityIntervalInputEvent"
);
}
}
);
return result;
}
private List<TachographEsperActivityIntervalEvent> buildEsperDrivingIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals
) {
if (activityIntervals == null || activityIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperActivityIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographActivityIntervalInputEvent",
activityIntervalInputDefinition()
),
DRIVING_INTERVAL_EVENTS_EPL,
"drivingIntervals",
newData -> collectActivityIntervalEvents(newData, result),
runtime -> {
for (ResolvedActivityInterval interval : activityIntervals) {
runtime.getEventService().sendEventMap(
toActivityIntervalInputMap(sessionId, driverKey, interval),
"TachographActivityIntervalInputEvent"
);
}
}
);
return result;
}
private List<TachographEsperVehicleUsageIntervalEvent> buildEsperVehicleUsageIntervalEvents(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals
) {
if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) {
return List.of();
}
List<TachographEsperVehicleUsageIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographVehicleUsageIntervalInputEvent",
vehicleUsageIntervalInputDefinition()
),
VEHICLE_USAGE_INTERVAL_EVENTS_EPL,
"vehicleUsageIntervals",
newData -> collectVehicleUsageIntervalEvents(newData, result),
runtime -> {
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
runtime.getEventService().sendEventMap(
toVehicleUsageIntervalInputMap(interval),
"TachographVehicleUsageIntervalInputEvent"
);
}
}
);
return result;
}
private List<TachographEsperDrivingInterruptionIntervalEvent> buildEsperDrivingInterruptionIntervalEvents(
UUID sessionId,
String driverKey,
List<ResolvedActivityInterval> activityIntervals,
int significantDrivingMinutes
) {
if (activityIntervals == null || activityIntervals.size() < 2) {
return List.of();
}
List<TachographEsperDrivingInterruptionIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographActivityIntervalInputEvent",
activityIntervalInputDefinition()
),
renderDrivingInterruptionIntervalEventsEpl(significantDrivingMinutes),
"drivingInterruptionIntervals",
newData -> collectDrivingInterruptionIntervalEvents(newData, result),
runtime -> {
for (ResolvedActivityInterval interval : activityIntervals) {
runtime.getEventService().sendEventMap(
toActivityIntervalInputMap(sessionId, driverKey, interval),
"TachographActivityIntervalInputEvent"
);
}
}
);
return result;
}
private List<TachographEsperVuCardAbsentIntervalEvent> buildEsperVuCardAbsentIntervalEvents(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals
) {
if (vehicleUsageIntervals == null || vehicleUsageIntervals.size() < 2) {
return List.of();
}
List<TachographEsperVuCardAbsentIntervalEvent> result = new ArrayList<>();
executeWithRuntime(
configuration -> configuration.getCommon().addEventType(
"TachographVehicleUsageIntervalInputEvent",
vehicleUsageIntervalInputDefinition()
),
VU_CARD_ABSENT_INTERVAL_EVENTS_EPL,
"vuCardAbsentIntervals",
newData -> collectVuCardAbsentIntervalEvents(newData, result),
runtime -> {
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
runtime.getEventService().sendEventMap(
toVehicleUsageIntervalInputMap(interval),
"TachographVehicleUsageIntervalInputEvent"
);
}
}
);
return result;
}
private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(
UUID sessionId,
String driverKey,
List<ExtractedCardVehicleUsageInterval> rawIntervals,
String sourceKind
) {
@ -54,6 +455,8 @@ public class DriverTimelineBuilder {
List<ResolvedVehicleUsageInterval> sorted = rawIntervals.stream()
.filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from())))
.map(interval -> ResolvedVehicleUsageInterval.resolved(
sessionId,
driverKey,
interval.intervalId(),
interval.from(),
interval.to(),
@ -79,6 +482,8 @@ public class DriverTimelineBuilder {
if (canMerge(current, next)) {
currentSources.addAll(next.sourceIntervalIds());
current = ResolvedVehicleUsageInterval.resolved(
current.sessionId(),
current.driverKey(),
current.intervalId() + "+" + next.intervalId(),
current.from(),
mergedTo(current.to(), next.to()),
@ -220,4 +625,417 @@ public class DriverTimelineBuilder {
private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) {
return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1);
}
private void executeWithRuntime(
Consumer<Configuration> configurationSetup,
String epl,
String statementName,
Consumer<EventBean[]> listener,
Consumer<EPRuntime> sender
) {
EPRuntime runtime = null;
try {
Configuration configuration = new Configuration();
configurationSetup.accept(configuration);
String runtimeUri = "eventhub-tachograph-projection-" + RUNTIME_COUNTER.incrementAndGet();
runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration);
CompilerArguments arguments = new CompilerArguments(configuration);
EPCompiled compiled = EPCompilerProvider.getCompiler().compile(epl, arguments);
EPDeployment deployment = runtime.getDeploymentService().deploy(compiled);
runtime.getDeploymentService()
.getStatement(deployment.getDeploymentId(), statementName)
.addListener((newData, oldData, statement, rt) -> listener.accept(newData));
sender.accept(runtime);
} catch (EPCompileException | EPDeployException e) {
throw new IllegalStateException(
"Cannot compile/deploy tachograph projection EPL for statement '" + statementName + "'",
e
);
} finally {
if (runtime != null) {
runtime.destroy();
}
}
}
private Map<String, Object> activityIntervalInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("intervalId", String.class);
definition.put("activityType", String.class);
definition.put("cardSlot", String.class);
definition.put("cardStatus", String.class);
definition.put("drivingStatus", String.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", long.class);
definition.put("durationSeconds", long.class);
definition.put("sourceIntervalIds", java.util.List.class);
definition.put("synthetic", boolean.class);
definition.put("clippedToRequestedPeriod", boolean.class);
definition.put("level", String.class);
return definition;
}
private Map<String, Object> vehicleUsageIntervalInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("intervalId", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", Long.class);
definition.put("durationSeconds", long.class);
definition.put("odometerBeginKm", Long.class);
definition.put("odometerEndKm", Long.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("sourceIntervalIds", java.util.List.class);
return definition;
}
private Map<String, Object> drivingInterruptionIntervalInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", long.class);
definition.put("durationSeconds", long.class);
definition.put("previousDrivingSourceIntervalId", String.class);
definition.put("nextDrivingSourceIntervalId", String.class);
definition.put("previousRegistrationKey", String.class);
definition.put("nextRegistrationKey", String.class);
definition.put("previousVehicleKey", String.class);
definition.put("nextVehicleKey", String.class);
return definition;
}
private Map<String, Object> vuCardAbsentIntervalInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", long.class);
definition.put("durationSeconds", long.class);
definition.put("previousUsageIntervalId", String.class);
definition.put("nextUsageIntervalId", String.class);
definition.put("previousRegistrationKey", String.class);
definition.put("nextRegistrationKey", String.class);
definition.put("previousVehicleKey", String.class);
definition.put("nextVehicleKey", String.class);
return definition;
}
private Map<String, Object> toActivityIntervalInputMap(
UUID sessionId,
String driverKey,
ResolvedActivityInterval interval
) {
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId);
event.put("driverKey", driverKey);
event.put("intervalId", interval.intervalId());
event.put("activityType", interval.activityType());
event.put("cardSlot", interval.slot());
event.put("cardStatus", interval.cardStatus());
event.put("drivingStatus", interval.drivingStatus());
event.put("registrationKey", interval.registrationKey());
event.put("vehicleKey", interval.vehicleKey());
event.put("sourceKind", interval.sourceKind());
event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
event.put("startedAt", interval.from());
event.put("endedAt", interval.to());
event.put("startedAtEpochSecond", interval.from().toEpochSecond());
event.put("endedAtEpochSecond", interval.to().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds());
event.put("sourceIntervalIds", interval.sourceIntervalIds());
event.put("synthetic", interval.synthetic());
event.put("clippedToRequestedPeriod", interval.clippedToRequestedPeriod());
event.put("level", interval.level());
return event;
}
private String firstSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
}
private String lastSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty()
? interval.intervalId()
: interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
}
private Map<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) {
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", interval.sessionId());
event.put("driverKey", interval.driverKey());
event.put("intervalId", interval.intervalId());
event.put("firstSourceIntervalId", firstSourceIntervalId(interval));
event.put("lastSourceIntervalId", lastSourceIntervalId(interval));
event.put("startedAt", interval.from());
event.put("endedAt", interval.to());
event.put("startedAtEpochSecond", interval.from().toEpochSecond());
event.put("endedAtEpochSecond", interval.to() == null ? null : interval.to().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds());
event.put("odometerBeginKm", interval.odometerBeginKm());
event.put("odometerEndKm", interval.odometerEndKm());
event.put("registrationKey", interval.registrationKey());
event.put("vehicleKey", interval.vehicleKey());
event.put("sourceKind", interval.sourceKind());
event.put("sourceIntervalIds", interval.sourceIntervalIds());
return event;
}
private Map<String, Object> toDrivingInterruptionIntervalInputMap(
TachographEsperDrivingInterruptionIntervalEvent interval
) {
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", interval.sessionId());
event.put("driverKey", interval.driverKey());
event.put("startedAt", interval.startedAt());
event.put("endedAt", interval.endedAt());
event.put("startedAtEpochSecond", interval.startedAt().toEpochSecond());
event.put("endedAtEpochSecond", interval.endedAt().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds());
event.put("previousDrivingSourceIntervalId", interval.previousDrivingSourceIntervalId());
event.put("nextDrivingSourceIntervalId", interval.nextDrivingSourceIntervalId());
event.put("previousRegistrationKey", interval.previousRegistrationKey());
event.put("nextRegistrationKey", interval.nextRegistrationKey());
event.put("previousVehicleKey", interval.previousVehicleKey());
event.put("nextVehicleKey", interval.nextVehicleKey());
return event;
}
private Map<String, Object> toVuCardAbsentIntervalInputMap(TachographEsperVuCardAbsentIntervalEvent interval) {
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", interval.sessionId());
event.put("driverKey", interval.driverKey());
event.put("startedAt", interval.startedAt());
event.put("endedAt", interval.endedAt());
event.put("startedAtEpochSecond", interval.startedAt().toEpochSecond());
event.put("endedAtEpochSecond", interval.endedAt().toEpochSecond());
event.put("durationSeconds", interval.durationSeconds());
event.put("previousUsageIntervalId", interval.previousUsageIntervalId());
event.put("nextUsageIntervalId", interval.nextUsageIntervalId());
event.put("previousRegistrationKey", interval.previousRegistrationKey());
event.put("nextRegistrationKey", interval.nextRegistrationKey());
event.put("previousVehicleKey", interval.previousVehicleKey());
event.put("nextVehicleKey", interval.nextVehicleKey());
return event;
}
private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
}
private String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) {
return interval.sourceIntervalIds().isEmpty()
? interval.intervalId()
: interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1);
}
private void collectActivityIntervalEvents(
EventBean[] newData,
List<TachographEsperActivityIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
target.add(new TachographEsperActivityIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
(String) event.get("intervalId"),
(String) event.get("activityType"),
(String) event.get("cardSlot"),
(String) event.get("cardStatus"),
(String) event.get("drivingStatus"),
(String) event.get("registrationKey"),
(String) event.get("vehicleKey"),
(String) event.get("sourceKind"),
(OffsetDateTime) event.get("startedAt"),
(OffsetDateTime) event.get("endedAt"),
(Long) event.get("durationSeconds"),
castSourceIntervalIds(event.get("sourceIntervalIds")),
(Boolean) event.get("synthetic"),
(Boolean) event.get("clippedToRequestedPeriod"),
(String) event.get("level")
));
}
}
private void collectVehicleUsageIntervalEvents(
EventBean[] newData,
List<TachographEsperVehicleUsageIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
target.add(new TachographEsperVehicleUsageIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
(String) event.get("intervalId"),
(OffsetDateTime) event.get("startedAt"),
(OffsetDateTime) event.get("endedAt"),
(Long) event.get("durationSeconds"),
(Long) event.get("odometerBeginKm"),
(Long) event.get("odometerEndKm"),
(String) event.get("registrationKey"),
(String) event.get("vehicleKey"),
(String) event.get("sourceKind"),
castSourceIntervalIds(event.get("sourceIntervalIds"))
));
}
}
private void collectDrivingInterruptionIntervalEvents(
EventBean[] newData,
List<TachographEsperDrivingInterruptionIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperDrivingInterruptionIntervalEvent(
(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("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private void collectDrivingInterruptionIntervalEventsFromTimestamps(
EventBean[] newData,
List<TachographEsperDrivingInterruptionIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
target.add(new TachographEsperDrivingInterruptionIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
(OffsetDateTime) event.get("startedAt"),
(OffsetDateTime) event.get("endedAt"),
(Long) event.get("durationSeconds"),
(String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private void collectVuCardAbsentIntervalEvents(
EventBean[] newData,
List<TachographEsperVuCardAbsentIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
target.add(new TachographEsperVuCardAbsentIntervalEvent(
(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("previousUsageIntervalId"),
(String) event.get("nextUsageIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
private void collectPotentialHomeOvernightStayIntervalEvents(
EventBean[] newData,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> target
) {
if (newData == null) {
return;
}
for (EventBean event : newData) {
target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
(UUID) event.get("sessionId"),
(String) event.get("driverKey"),
(OffsetDateTime) event.get("startedAt"),
(OffsetDateTime) event.get("endedAt"),
(Long) event.get("durationSeconds"),
(Long) event.get("unknownDurationSeconds"),
(Double) event.get("unknownCoveragePercent"),
(String) event.get("previousDrivingSourceIntervalId"),
(String) event.get("nextDrivingSourceIntervalId"),
(String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey")
));
}
}
@SuppressWarnings("unchecked")
private List<String> castSourceIntervalIds(Object value) {
return value == null ? List.of() : List.copyOf((List<String>) value);
}
private static String loadResource(String path) {
try {
ClassPathResource resource = new ClassPathResource(path);
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException("Cannot load EPL resource: " + path, e);
}
}
private String renderDrivingInterruptionIntervalEventsEpl(int significantDrivingMinutes) {
long thresholdSeconds = Math.max(1, significantDrivingMinutes) * 60L;
return DRIVING_INTERRUPTION_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}",
Long.toString(thresholdSeconds)
);
}
private String renderDailyWeeklyRestCandidateIntervalEventsEpl(int minimumRestPeriodMinutes) {
long thresholdSeconds = Math.max(1, minimumRestPeriodMinutes) * 60L;
return DAILY_WEEKLY_REST_CANDIDATE_INTERVAL_EVENTS_EPL_TEMPLATE.replace(
"${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}",
Long.toString(thresholdSeconds)
);
}
}

View File

@ -1,6 +1,8 @@
package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
@ -10,7 +12,12 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedOperatingPeriod;
import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvaluation;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
@ -113,6 +120,334 @@ public class TachographFileSessionProcessingService {
);
}
public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults(
UUID sessionId,
String driverKey
) {
return getEsperDriverProcessingResults(sessionId, driverKey, null);
}
public TachographEsperDriverProcessingResultDto getEsperDriverProcessingResults(
UUID sessionId,
String driverKey,
TachographEsperEventsProcessingRequest request
) {
TachographEsperEventsProcessingRequest effectiveRequest = request == null
? new TachographEsperEventsProcessingRequest(null, null, null, null)
: request;
TachographFileSession session = repository.find(sessionId)
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
DriverExtractionSession driver = session.driversByKey().get(driverKey);
if (driver == null) {
throw new DriverNotFoundInSessionException(sessionId, driverKey);
}
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
}
int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest);
int minimumRestPeriodMinutes = resolveMinimumRestPeriodMinutes(effectiveRequest);
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents(
sessionId,
driverKey,
timeline,
significantDrivingMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDrivingInterruptionIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(
rawDrivingInterruptionIntervals,
minimumRestPeriodMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDailyWeeklyRestCandidateIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDailyWeeklyRestCandidateIntervals
);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDrivingInterruptionVehicleChangeIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
rawDrivingInterruptionVehicleChangeIntervals,
rawVuCardAbsentIntervals
),
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline),
requestedFrom,
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo
);
return new TachographEsperDriverProcessingResultDto(
sessionId,
driverKey,
timeline.sourceKind(),
timeline.loadedFrom(),
timeline.loadedTo(),
requestedFrom,
requestedTo,
activityIntervals.size(),
drivingIntervals.size(),
drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(),
activityIntervals,
drivingIntervals,
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
potentialHomeOvernightStayIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
esperProjectionNotes()
);
}
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
List<TachographEsperActivityIntervalEvent> intervals,
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;
}
boolean clipped = interval.clippedToRequestedPeriod()
|| !start.equals(interval.startedAt())
|| !end.equals(interval.endedAt());
return new TachographEsperActivityIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
interval.activityType(),
interval.cardSlot(),
interval.cardStatus(),
interval.drivingStatus(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.sourceIntervalIds(),
interval.synthetic(),
clipped,
interval.level()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
.thenComparing(TachographEsperActivityIntervalEvent::endedAt)
.thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
List<TachographEsperVehicleUsageIntervalEvent> intervals,
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;
}
boolean startClipped = !start.equals(interval.startedAt());
boolean endClipped = !end.equals(interval.endedAt());
return new TachographEsperVehicleUsageIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
start,
end,
Duration.between(start, end).getSeconds(),
startClipped ? null : interval.odometerBeginKm(),
endClipped ? null : interval.odometerEndKm(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
interval.sourceIntervalIds()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
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;
}
return new TachographEsperDrivingInterruptionIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
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;
}
return new TachographEsperVuCardAbsentIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousUsageIntervalId(),
interval.nextUsageIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> 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<ResolvedActivityInterval> synthesizeUnknownGaps(
List<ResolvedActivityInterval> knownIntervals,
Duration gapDetectionTolerance
@ -651,6 +986,18 @@ public class TachographFileSessionProcessingService {
: request.gapDetectionToleranceSeconds();
}
private int resolveEsperSignificantDrivingMinutes(TachographEsperEventsProcessingRequest request) {
return request.significantDrivingMinutes() == null
? properties.getTachographFileSession().getProcessing().getSignificantDrivingMinutes()
: request.significantDrivingMinutes();
}
private int resolveMinimumRestPeriodMinutes(TachographEsperEventsProcessingRequest request) {
return request.minimumRestPeriodMinutes() == null
? properties.getTachographFileSession().getProcessing().getMinimumRestPeriodMinutes()
: request.minimumRestPeriodMinutes();
}
private List<String> notes() {
return List.of(
"This endpoint evaluates operating periods from the in-memory tachograph file-session model.",
@ -660,6 +1007,43 @@ public class TachographFileSessionProcessingService {
);
}
private List<String> esperProjectionNotes() {
return List.of(
"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 interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"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.",
"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."
);
}
private long overlapSeconds(
OffsetDateTime intervalStart,
OffsetDateTime intervalEnd,
List<TachographEsperVuCardAbsentIntervalEvent> unknownIntervals,
String driverKey
) {
if (unknownIntervals == null || unknownIntervals.isEmpty()) {
return 0L;
}
long total = 0L;
for (TachographEsperVuCardAbsentIntervalEvent unknown : unknownIntervals) {
if (!Objects.equals(driverKey, unknown.driverKey())) {
continue;
}
OffsetDateTime overlapStart = max(intervalStart, unknown.startedAt());
OffsetDateTime overlapEnd = min(intervalEnd, unknown.endedAt());
if (overlapEnd.isAfter(overlapStart)) {
total += Duration.between(overlapStart, overlapEnd).getSeconds();
}
}
return total;
}
private OffsetDateTime utc(OffsetDateTime value) {
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
}

View File

@ -0,0 +1,20 @@
@name('activityIntervals')
select
sessionId,
driverKey,
intervalId,
activityType,
cardSlot,
cardStatus,
drivingStatus,
registrationKey,
vehicleKey,
sourceKind,
startedAt,
endedAt,
durationSeconds,
sourceIntervalIds,
synthetic,
clippedToRequestedPeriod,
level
from TachographActivityIntervalInputEvent

View File

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

View File

@ -0,0 +1,70 @@
create schema SignificantDrivingInterval(
sessionId java.util.UUID,
driverKey string,
firstSourceIntervalId string,
lastSourceIntervalId string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
registrationKey string,
vehicleKey string
);
create schema DrivingInterruptionInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
previousDrivingSourceIntervalId string,
nextDrivingSourceIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
insert into SignificantDrivingInterval
select
sessionId,
driverKey,
firstSourceIntervalId,
lastSourceIntervalId,
startedAtEpochSecond,
endedAtEpochSecond,
durationSeconds,
registrationKey,
vehicleKey
from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS});
create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval;
on SignificantDrivingInterval as next
insert into DrivingInterruptionInterval
select
priorInterval.sessionId as sessionId,
priorInterval.driverKey as driverKey,
priorInterval.endedAtEpochSecond as startedAtEpochSecond,
next.startedAtEpochSecond as endedAtEpochSecond,
next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds,
priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId,
next.firstSourceIntervalId as nextDrivingSourceIntervalId,
priorInterval.registrationKey as previousRegistrationKey,
next.registrationKey as nextRegistrationKey,
priorInterval.vehicleKey as previousVehicleKey,
next.vehicleKey as nextVehicleKey
from PreviousSignificantDrivingInterval as priorInterval
where priorInterval.driverKey = next.driverKey
and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond;
@Priority(20)
on SignificantDrivingInterval
delete from PreviousSignificantDrivingInterval;
@Priority(10)
on SignificantDrivingInterval as current
insert into PreviousSignificantDrivingInterval
select *;
@name('drivingInterruptionIntervals')
select * from DrivingInterruptionInterval;

View File

@ -0,0 +1,18 @@
@name('drivingInterruptionVehicleChangeIntervals')
select
sessionId,
driverKey,
startedAt,
endedAt,
durationSeconds,
previousDrivingSourceIntervalId,
nextDrivingSourceIntervalId,
previousRegistrationKey,
nextRegistrationKey,
previousVehicleKey,
nextVehicleKey
from TachographDrivingInterruptionIntervalInputEvent(
previousRegistrationKey is not null,
nextRegistrationKey is not null,
previousRegistrationKey != nextRegistrationKey
);

View File

@ -0,0 +1,20 @@
@name('drivingIntervals')
select
sessionId,
driverKey,
intervalId,
activityType,
cardSlot,
cardStatus,
drivingStatus,
registrationKey,
vehicleKey,
sourceKind,
startedAt,
endedAt,
durationSeconds,
sourceIntervalIds,
synthetic,
clippedToRequestedPeriod,
level
from TachographActivityIntervalInputEvent(activityType = 'DRIVE')

View File

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

View File

@ -0,0 +1,15 @@
@name('vehicleUsageIntervals')
select
sessionId,
driverKey,
intervalId,
startedAt,
endedAt,
durationSeconds,
odometerBeginKm,
odometerEndKm,
registrationKey,
vehicleKey,
sourceKind,
sourceIntervalIds
from TachographVehicleUsageIntervalInputEvent

View File

@ -0,0 +1,53 @@
create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent;
create schema VuCardAbsentInterval(
sessionId java.util.UUID,
driverKey string,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
previousUsageIntervalId string,
nextUsageIntervalId string,
previousRegistrationKey string,
nextRegistrationKey string,
previousVehicleKey string,
nextVehicleKey string
);
context PerDriver
create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent;
@Priority(30)
context PerDriver
on TachographVehicleUsageIntervalInputEvent as next
insert into VuCardAbsentInterval
select
priorInterval.sessionId as sessionId,
priorInterval.driverKey as driverKey,
priorInterval.endedAtEpochSecond + 1L as startedAtEpochSecond,
next.startedAtEpochSecond as endedAtEpochSecond,
next.startedAtEpochSecond - (priorInterval.endedAtEpochSecond + 1L) as durationSeconds,
priorInterval.lastSourceIntervalId as previousUsageIntervalId,
next.firstSourceIntervalId as nextUsageIntervalId,
priorInterval.registrationKey as previousRegistrationKey,
next.registrationKey as nextRegistrationKey,
priorInterval.vehicleKey as previousVehicleKey,
next.vehicleKey as nextVehicleKey
from PreviousVehicleUsageInterval as priorInterval
where priorInterval.endedAt is not null
and next.startedAt is not null
and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond + 1L;
@Priority(20)
context PerDriver
on TachographVehicleUsageIntervalInputEvent
delete from PreviousVehicleUsageInterval;
@Priority(10)
context PerDriver
on TachographVehicleUsageIntervalInputEvent as current
insert into PreviousVehicleUsageInterval
select *;
@name('vuCardAbsentIntervals')
select * from VuCardAbsentInterval;

View File

@ -9,6 +9,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
@ -17,8 +19,14 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteR
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService;
import java.time.OffsetDateTime;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@ -58,6 +66,148 @@ class TachographFileSessionControllerTest {
when(service.getSession(sessionId)).thenReturn(summary);
when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver)));
when(service.getDriver(sessionId, "12:123")).thenReturn(new TachographFileDriverDetailDto(sessionId, "12:123", null, null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of()));
when(processingService.getEsperDriverProcessingResults(
eq(sessionId),
eq("12:123"),
org.mockito.ArgumentMatchers.any(TachographEsperEventsProcessingRequest.class)
))
.thenReturn(new TachographEsperDriverProcessingResultDto(
sessionId,
"12:123",
"DRIVER_CARD",
OffsetDateTime.parse("2026-05-12T08:00:00Z"),
OffsetDateTime.parse("2026-05-12T12:00:00Z"),
OffsetDateTime.parse("2026-05-12T08:30:00Z"),
OffsetDateTime.parse("2026-05-12T11:30:00Z"),
2,
1,
1,
1,
1,
1,
2,
1,
List.of(new TachographEsperActivityIntervalEvent(
sessionId,
"12:123",
"ACT-1",
"WORK",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"DRIVER_CARD",
OffsetDateTime.parse("2026-05-12T08:00:00Z"),
OffsetDateTime.parse("2026-05-12T09:00:00Z"),
3600L,
List.of("ACT-1"),
false,
false,
"RAW_INTERVAL"
)),
List.of(new TachographEsperActivityIntervalEvent(
sessionId,
"12:123",
"ACT-2",
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"DRIVER_CARD",
OffsetDateTime.parse("2026-05-12T09:00:00Z"),
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
3600L,
List.of("ACT-2"),
false,
false,
"RAW_INTERVAL"
)),
List.of(new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T10:30:00Z"),
1800L,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T10:30:00Z"),
1800L,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"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(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
OffsetDateTime.parse("2026-05-12T22:00:00Z"),
43_200L,
43_200L,
100.0d,
"ACT-2",
"ACT-3",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of(new TachographEsperVehicleUsageIntervalEvent(
sessionId,
"12:123",
"CVU-1",
OffsetDateTime.parse("2026-05-12T08:00:00Z"),
OffsetDateTime.parse("2026-05-12T10:00:00Z"),
7200L,
100L,
200L,
"12:REG-1",
"VIN-1",
"DRIVER_CARD",
List.of("CVU-1")
)),
List.of(new TachographEsperVuCardAbsentIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-12T10:00:01Z"),
OffsetDateTime.parse("2026-05-12T11:00:00Z"),
3599L,
"CVU-1",
"CVU-2",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)),
List.of("note")
));
when(processingService.evaluateOperatingPeriods(eq(sessionId), eq("12:123"), org.mockito.ArgumentMatchers.any(TachographOperatingPeriodsProcessingRequest.class)))
.thenReturn(new TachographOperatingPeriodsProcessingResultDto(
sessionId,
@ -104,6 +254,37 @@ class TachographFileSessionControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.driverKey").value("12:123"));
mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events", sessionId, "12:123")
.contentType("application/json")
.content("""
{
"occurredFrom": "2026-05-12T08:30:00Z",
"occurredTo": "2026-05-12T11:30:00Z",
"significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.driverKey").value("12:123"))
.andExpect(jsonPath("$.sourceKind").value("DRIVER_CARD"))
.andExpect(jsonPath("$.requestedFrom").value("2026-05-12T08:30:00Z"))
.andExpect(jsonPath("$.requestedTo").value("2026-05-12T11:30:00Z"))
.andExpect(jsonPath("$.activityIntervalCount").value(2))
.andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1))
.andExpect(jsonPath("$.dailyWeeklyRestCandidateIntervalCount").value(1))
.andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousVehicleKey").value("VIN-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextVehicleKey").value("VIN-2"))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].previousRegistrationKey").value("12:REG-1"))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].nextRegistrationKey").value("12:REG-2"))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].previousVehicleKey").value("VIN-1"))
.andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervals[0].nextVehicleKey").value("VIN-2"))
.andExpect(jsonPath("$.drivingIntervals[0].activityType").value("DRIVE"));
mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/operating-periods", sessionId, "12:123")
.contentType("application/json")
.content("""

View File

@ -9,6 +9,11 @@ import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import java.time.Instant;
@ -100,6 +105,8 @@ class DriverTimelineBuilderTest {
assertThat(timeline.sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(timeline.vehicleUsageIntervals()).hasSize(1);
assertThat(timeline.vehicleUsageIntervals().get(0).sessionId()).isEqualTo(session.sessionId());
assertThat(timeline.vehicleUsageIntervals().get(0).driverKey()).isEqualTo("12:123");
assertThat(timeline.vehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));
assertThat(timeline.vehicleUsageIntervals().get(0).to()).isEqualTo(OffsetDateTime.parse("2026-05-02T08:00:00Z"));
assertThat(timeline.activityIntervals()).hasSize(1);
@ -169,9 +176,389 @@ class DriverTimelineBuilderTest {
ResolvedDriverTimeline timeline = builder.build(session, driver);
assertThat(timeline.vehicleUsageIntervals()).hasSize(1);
assertThat(timeline.vehicleUsageIntervals().get(0).sessionId()).isEqualTo(session.sessionId());
assertThat(timeline.vehicleUsageIntervals().get(0).driverKey()).isEqualTo("12:123");
assertThat(timeline.vehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-05-01T00:00:00Z"));
assertThat(timeline.vehicleUsageIntervals().get(0).to()).isNull();
assertThat(timeline.vehicleUsageIntervals().get(0).sourceIntervalIds()).containsExactly("CVU-1", "CVU-2");
assertThat(timeline.loadedTo()).isEqualTo(OffsetDateTime.parse("2026-05-02T09:00:00Z"));
}
@Test
void buildsEsperActivityAndDrivingIntervalEventsFromResolvedTimeline() {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
null,
null,
List.of(),
List.of(),
List.of(),
List.of(
new ExtractedCardActivityInterval(
"ACT-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
"WORK",
"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:30:00Z"),
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"b"
)
),
List.of(),
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, 2, 0, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
List<TachographEsperActivityIntervalEvent> activityEvents =
builder.buildEsperActivityIntervalEvents(session, driver);
List<TachographEsperActivityIntervalEvent> drivingEvents =
builder.buildEsperDrivingIntervalEvents(session, driver);
assertThat(activityEvents).hasSize(2);
assertThat(activityEvents).extracting(TachographEsperActivityIntervalEvent::activityType)
.containsExactly("WORK", "DRIVE");
assertThat(activityEvents).extracting(TachographEsperActivityIntervalEvent::driverKey)
.containsOnly("12:123");
assertThat(drivingEvents).hasSize(1);
assertThat(drivingEvents.get(0).intervalId()).isEqualTo("ACT-2");
assertThat(drivingEvents.get(0).activityType()).isEqualTo("DRIVE");
assertThat(drivingEvents.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z"));
assertThat(drivingEvents.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:30:00Z"));
}
@Test
void buildsEsperVehicleUsageIntervalEventsFromResolvedTimeline() {
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",
"a"
),
new ExtractedCardVehicleUsageInterval(
"CVU-2",
OffsetDateTime.parse("2026-05-01T12:00:00Z"),
null,
201L,
null,
"12:REG-1",
"VIN-1",
"b"
)
),
List.of(),
List.of(),
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, 0, 2, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageEvents =
builder.buildEsperVehicleUsageIntervalEvents(session, driver);
assertThat(vehicleUsageEvents).hasSize(2);
assertThat(vehicleUsageEvents).extracting(TachographEsperVehicleUsageIntervalEvent::driverKey)
.containsOnly("12:123");
assertThat(vehicleUsageEvents).extracting(TachographEsperVehicleUsageIntervalEvent::sessionId)
.containsOnly(session.sessionId());
assertThat(vehicleUsageEvents.get(0).intervalId()).isEqualTo("CVU-1");
assertThat(vehicleUsageEvents.get(1).intervalId()).isEqualTo("CVU-2");
assertThat(vehicleUsageEvents.get(1).endedAt()).isNull();
}
@Test
void buildsEsperVuCardAbsentIntervalEventsFromVehicleUsageGaps() {
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",
"a"
),
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",
"b"
),
new ExtractedCardVehicleUsageInterval(
"CVU-3",
OffsetDateTime.parse("2026-05-01T13:00:01Z"),
OffsetDateTime.parse("2026-05-01T14:00:00Z"),
261L,
320L,
"12:REG-2",
"VIN-2",
"c"
)
),
List.of(),
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, 0, 3, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
List<TachographEsperVuCardAbsentIntervalEvent> absentIntervals =
builder.buildEsperVuCardAbsentIntervalEvents(session, driver);
assertThat(absentIntervals).hasSize(1);
assertThat(absentIntervals.get(0).sessionId()).isEqualTo(session.sessionId());
assertThat(absentIntervals.get(0).driverKey()).isEqualTo("12:123");
assertThat(absentIntervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:01Z"));
assertThat(absentIntervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z"));
assertThat(absentIntervals.get(0).durationSeconds()).isEqualTo(3599L);
assertThat(absentIntervals.get(0).previousUsageIntervalId()).isEqualTo("CVU-1");
assertThat(absentIntervals.get(0).nextUsageIntervalId()).isEqualTo("CVU-2");
assertThat(absentIntervals.get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(absentIntervals.get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(absentIntervals.get(0).previousVehicleKey()).isEqualTo("VIN-1");
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
void buildsEsperDrivingInterruptionIntervalEventsFromSignificantDrivingGaps() {
DriverExtractionSession driver = new DriverExtractionSession(
"12:123",
null,
null,
List.of(),
List.of(),
List.of(),
List.of(
new ExtractedCardActivityInterval(
"ACT-1",
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
OffsetDateTime.parse("2026-05-01T08:02:00Z"),
"DRIVE",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"a"
),
new ExtractedCardActivityInterval(
"ACT-2",
OffsetDateTime.parse("2026-05-01T08:02:00Z"),
OffsetDateTime.parse("2026-05-01T08:10:00Z"),
"WORK",
"DRIVER",
"INSERTED",
"SINGLE",
"12:REG-1",
"VIN-1",
"b"
),
new ExtractedCardActivityInterval(
"ACT-3",
OffsetDateTime.parse("2026-05-01T08:10:00Z"),
OffsetDateTime.parse("2026-05-01T08:15: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, 3, 0, 0, 0, 0),
List.of(),
Instant.now(),
Instant.now().plus(4, ChronoUnit.HOURS)
);
List<TachographEsperDrivingInterruptionIntervalEvent> interruptions =
builder.buildEsperDrivingInterruptionIntervalEvents(session, driver, 1);
assertThat(interruptions).hasSize(1);
assertThat(interruptions.get(0).sessionId()).isEqualTo(session.sessionId());
assertThat(interruptions.get(0).driverKey()).isEqualTo("12:123");
assertThat(interruptions.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:02:00Z"));
assertThat(interruptions.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:10:00Z"));
assertThat(interruptions.get(0).durationSeconds()).isEqualTo(480L);
assertThat(interruptions.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1");
assertThat(interruptions.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-3");
assertThat(interruptions.get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(interruptions.get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(interruptions.get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(interruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2");
List<TachographEsperDrivingInterruptionIntervalEvent> vehicleChangeInterruptions =
builder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(interruptions);
assertThat(vehicleChangeInterruptions).hasSize(1);
assertThat(vehicleChangeInterruptions.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:02:00Z"));
assertThat(vehicleChangeInterruptions.get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(vehicleChangeInterruptions.get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(vehicleChangeInterruptions.get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(vehicleChangeInterruptions.get(0).nextVehicleKey()).isEqualTo("VIN-2");
}
@Test
void buildsPotentialHomeOvernightStayIntervalsFromDtiAndVuCardAbsentOverlap() {
UUID sessionId = UUID.randomUUID();
List<TachographEsperDrivingInterruptionIntervalEvent> interruptions = List.of(
new TachographEsperDrivingInterruptionIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
50_400L,
"ACT-1",
"ACT-2",
"12:REG-1",
"12:REG-2",
"VIN-1",
"VIN-2"
)
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = List.of(
new TachographEsperVuCardAbsentIntervalEvent(
sessionId,
"12:123",
OffsetDateTime.parse("2026-05-01T10:00:01Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
50_399L,
"CVU-1",
"CVU-2",
"12:REG-1",
"12:REG-1",
"VIN-1",
"VIN-1"
)
);
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
builder.buildEsperDailyWeeklyRestCandidateIntervalEvents(interruptions, 720);
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
builder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(dailyWeeklyRestCandidateIntervals);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals =
builder.buildEsperPotentialHomeOvernightStayIntervalEvents(
drivingInterruptionVehicleChangeIntervals,
vuCardAbsentIntervals
);
assertThat(drivingInterruptionVehicleChangeIntervals).hasSize(1);
assertThat(intervals).hasSize(1);
assertThat(intervals.get(0).sessionId()).isEqualTo(sessionId);
assertThat(intervals.get(0).driverKey()).isEqualTo("12:123");
assertThat(intervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(intervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
assertThat(intervals.get(0).durationSeconds()).isEqualTo(50_400L);
assertThat(intervals.get(0).unknownDurationSeconds()).isEqualTo(50_399L);
assertThat(intervals.get(0).unknownCoveragePercent()).isGreaterThan(99.9d);
assertThat(intervals.get(0).previousDrivingSourceIntervalId()).isEqualTo("ACT-1");
assertThat(intervals.get(0).nextDrivingSourceIntervalId()).isEqualTo("ACT-2");
}
}

View File

@ -3,6 +3,8 @@ package at.procon.eventhub.tachographfilesession.service;
import static org.assertj.core.api.Assertions.assertThat;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
@ -21,6 +23,251 @@ import org.junit.jupiter.api.Test;
class TachographFileSessionProcessingServiceTest {
@Test
void returnsEsperDriverProcessingResultsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
new DriverTimelineBuilder(),
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"), "WORK", "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"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b")
),
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());
assertThat(result.sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(result.activityIntervalCount()).isEqualTo(2);
assertThat(result.drivingIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0);
assertThat(result.potentialHomeOvernightStayIntervalCount()).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"));
assertThat(result.vuCardAbsentIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:00Z"));
}
@Test
void appliesOccurredWindowToEsperDriverProcessingResults() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
new DriverTimelineBuilder(),
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.requestedFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.requestedTo()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:30:00Z"));
assertThat(result.activityIntervalCount()).isEqualTo(3);
assertThat(result.activityIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:45:00Z"));
assertThat(result.activityIntervals().get(0).clippedToRequestedPeriod()).isTrue();
assertThat(result.drivingIntervalCount()).isEqualTo(2);
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0);
assertThat(result.dailyWeeklyRestCandidateIntervalCount()).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).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
assertThat(result.drivingInterruptionIntervals().get(0).previousRegistrationKey()).isEqualTo("12:REG-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextRegistrationKey()).isEqualTo("12:REG-2");
assertThat(result.drivingInterruptionIntervals().get(0).previousVehicleKey()).isEqualTo("VIN-1");
assertThat(result.drivingInterruptionIntervals().get(0).nextVehicleKey()).isEqualTo("VIN-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).odometerBeginKm()).isNull();
assertThat(result.vehicleUsageIntervals().get(1).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T12:30:00Z"));
assertThat(result.vehicleUsageIntervals().get(1).odometerEndKm()).isNull();
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
}
@Test
void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() {
EventHubProperties properties = new EventHubProperties();
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository,
new DriverTimelineBuilder(),
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-01T10:00:00Z"),
100L,
200L,
"12:REG-1",
"VIN-1",
"vu-1"
),
new ExtractedCardVehicleUsageInterval(
"CVU-2",
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T02:00:00Z"),
201L,
260L,
"12:REG-1",
"VIN-1",
"vu-2"
)
),
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-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()
);
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, 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-01T11:00:00Z"),
OffsetDateTime.parse("2026-05-01T23:00:00Z"),
3,
720
)
);
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.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).unknownDurationSeconds()).isEqualTo(43_200L);
assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d);
}
@Test
void evaluatesOperatingPeriodsFromSessionTimeline() {
EventHubProperties properties = new EventHubProperties();