From 9b86d1300156c887a7d3454fd95f8d9951a6e513 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Thu, 21 May 2026 11:49:20 +0200 Subject: [PATCH] Add runtime processing API and rest coverage projections --- ...eventhub-esper-poc.postman_collection.json | 185 +++++++++ .../UnifiedRuntimeProcessingController.java | 42 ++ ...fiedRuntimeProcessingExceptionHandler.java | 36 ++ .../UnifiedRuntimeProcessingApiRequest.java | 40 ++ ...hographEsperDriverProcessingResultDto.java | 8 + ...klyRestCandidateCoverageIntervalEvent.java | 23 ++ ...phEsperDrivingDerivedProjectionBundle.java | 5 +- ...alInVehicleOvernightStayIntervalEvent.java | 21 + ...iverTimelineReusableProjectionBuilder.java | 94 ++++- ...ervalBackedDriverTimelineEventBuilder.java | 5 +- ...achographFileSessionProcessingService.java | 180 ++++++++- .../service/TachographFileSessionService.java | 26 ++ ...raph-driving-derived-projection-bundle.epl | 373 +++++++++++++++--- ...nifiedRuntimeProcessingControllerTest.java | 192 +++++++++ .../TachographFileSessionControllerTest.java | 26 ++ ...TimelineReusableProjectionBuilderTest.java | 92 +++++ ...graphFileSessionProcessingServiceTest.java | 190 +++++++++ 17 files changed, 1482 insertions(+), 56 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java create mode 100644 src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingExceptionHandler.java create mode 100644 src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java create mode 100644 src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java diff --git a/postman/eventhub-esper-poc.postman_collection.json b/postman/eventhub-esper-poc.postman_collection.json index 6489d25..784e338 100644 --- a/postman/eventhub-esper-poc.postman_collection.json +++ b/postman/eventhub-esper-poc.postman_collection.json @@ -458,6 +458,179 @@ } } ] + }, + { + "name": "Runtime processing", + "item": [ + { + "name": "Load runtime driver events from tachograph file session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_FILE_SESSION\"],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 0\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + } + } + }, + { + "name": "Load runtime driver timeline from tachograph file session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_FILE_SESSION\"],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 0\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + } + } + }, + { + "name": "Load runtime driver events from source DBs", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + } + } + }, + { + "name": "Load runtime driver timeline from source DBs", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + } + } + }, + { + "name": "Load runtime driver events from local EventHub DB", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + } + } + }, + { + "name": "Load runtime driver timeline from local EventHub DB", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\"TACHOGRAPH_DB\", \"YELLOWFOX_DB\"],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverSourceEntityId\": \"{{runtimeDriverSourceEntityId}}\",\n \"driverCardNation\": \"{{runtimeDriverCardNation}}\",\n \"driverCardNumber\": \"{{runtimeDriverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 30\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + } + } + } + ] } ], "variable": [ @@ -501,6 +674,18 @@ "key": "driverKey", "value": "12:12345678901200" }, + { + "key": "runtimeDriverSourceEntityId", + "value": "DRIVER:42" + }, + { + "key": "runtimeDriverCardNation", + "value": "12" + }, + { + "key": "runtimeDriverCardNumber", + "value": "12345678901200" + }, { "key": "tachographDddFile", "value": "C:\\\\temp\\\\driver-card.ddd" diff --git a/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java new file mode 100644 index 0000000..8ef3227 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java @@ -0,0 +1,42 @@ +package at.procon.eventhub.processing.api; + +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; +import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService; +import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/eventhub/runtime-processing") +public class UnifiedRuntimeProcessingController { + + private final UnifiedRuntimeEventAssemblyService eventAssemblyService; + private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService; + + public UnifiedRuntimeProcessingController( + UnifiedRuntimeEventAssemblyService eventAssemblyService, + UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService + ) { + this.eventAssemblyService = eventAssemblyService; + this.runtimeDriverTimelineService = runtimeDriverTimelineService; + } + + @PostMapping("/driver-events") + public ResponseEntity loadDriverEvents( + @RequestBody UnifiedRuntimeProcessingApiRequest request + ) { + return ResponseEntity.ok(eventAssemblyService.assembleDriverScopedEvents(request.toRuntimeRequest())); + } + + @PostMapping("/driver-timeline") + public ResponseEntity loadDriverTimeline( + @RequestBody UnifiedRuntimeProcessingApiRequest request + ) { + return ResponseEntity.ok(runtimeDriverTimelineService.loadDriverTimeline(request.toRuntimeRequest())); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingExceptionHandler.java b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingExceptionHandler.java new file mode 100644 index 0000000..acd7af6 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingExceptionHandler.java @@ -0,0 +1,36 @@ +package at.procon.eventhub.processing.api; + +import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException; +import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException; +import java.time.OffsetDateTime; +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice(basePackageClasses = UnifiedRuntimeProcessingController.class) +public class UnifiedRuntimeProcessingExceptionHandler { + + @ExceptionHandler({ + TachographFileSessionNotFoundException.class, + DriverNotFoundInSessionException.class + }) + public ResponseEntity> notFound(RuntimeException exception) { + return error(HttpStatus.NOT_FOUND, exception); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> badRequest(RuntimeException exception) { + return error(HttpStatus.BAD_REQUEST, exception); + } + + private ResponseEntity> error(HttpStatus status, RuntimeException exception) { + return ResponseEntity.status(status).body(Map.of( + "timestamp", OffsetDateTime.now().toString(), + "status", status.value(), + "error", status.getReasonPhrase(), + "message", exception.getMessage() + )); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java new file mode 100644 index 0000000..f6cd3c7 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeProcessingApiRequest.java @@ -0,0 +1,40 @@ +package at.procon.eventhub.processing.dto; + +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; +import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import java.time.OffsetDateTime; +import java.util.Set; +import java.util.UUID; + +public record UnifiedRuntimeProcessingApiRequest( + UUID sessionId, + String tenantKey, + Set sourceFamilies, + UnifiedRuntimeEventBackend eventBackend, + String driverKey, + String driverSourceEntityId, + String driverCardNation, + String driverCardNumber, + OffsetDateTime occurredFrom, + OffsetDateTime occurredTo, + Boolean expandVehicleEvents, + Integer vehicleExpansionPaddingMinutes +) { + public UnifiedRuntimeProcessingRequest toRuntimeRequest() { + return new UnifiedRuntimeProcessingRequest( + sessionId, + tenantKey, + sourceFamilies, + eventBackend, + driverKey, + driverSourceEntityId, + driverCardNation, + driverCardNumber, + occurredFrom, + occurredTo, + expandVehicleEvents == null || expandVehicleEvents, + vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes) + ); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java index 60d2869..09c05b7 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -1,8 +1,10 @@ package at.procon.eventhub.tachographfilesession.dto; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.OffsetDateTime; @@ -22,7 +24,10 @@ public record TachographEsperDriverProcessingResultDto( int drivingInterruptionIntervalCount, int drivingInterruptionVehicleChangeIntervalCount, int dailyWeeklyRestCandidateIntervalCount, + int dailyWeeklyRestCandidateCoverageIntervalCount, + int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount, int potentialHomeOvernightStayIntervalCount, + int potentialInVehicleOvernightStayIntervalCount, int vehicleUsageIntervalCount, int vuCardAbsentIntervalCount, List activityIntervals, @@ -30,7 +35,10 @@ public record TachographEsperDriverProcessingResultDto( List drivingInterruptionIntervals, List drivingInterruptionVehicleChangeIntervals, List dailyWeeklyRestCandidateIntervals, + List dailyWeeklyRestCandidateCoverageIntervals, + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, List potentialHomeOvernightStayIntervals, + List potentialInVehicleOvernightStayIntervals, List vehicleUsageIntervals, List vuCardAbsentIntervals, List notes diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java new file mode 100644 index 0000000..5a4f461 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent.java @@ -0,0 +1,23 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.UUID; + +public record TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + UUID sessionId, + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + long cardPresentDurationSeconds, + double cardPresentCoveragePercent, + long unknownDurationSeconds, + double unknownCoveragePercent, + String previousDrivingSourceIntervalId, + String nextDrivingSourceIntervalId, + String previousRegistrationKey, + String nextRegistrationKey, + String previousVehicleKey, + String nextVehicleKey +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java index a27dbd5..138df55 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java @@ -5,8 +5,11 @@ import java.util.List; public record TachographEsperDrivingDerivedProjectionBundle( List drivingInterruptionIntervals, List dailyWeeklyRestCandidateIntervals, + List dailyWeeklyRestCandidateCoverageIntervals, + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, List drivingInterruptionVehicleChangeIntervals, List vuCardAbsentIntervals, - List potentialHomeOvernightStayIntervals + List potentialHomeOvernightStayIntervals, + List potentialInVehicleOvernightStayIntervals ) { } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java new file mode 100644 index 0000000..3355e4d --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperPotentialInVehicleOvernightStayIntervalEvent.java @@ -0,0 +1,21 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.time.OffsetDateTime; +import java.util.UUID; + +public record TachographEsperPotentialInVehicleOvernightStayIntervalEvent( + UUID sessionId, + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + long durationSeconds, + long cardPresentDurationSeconds, + double cardPresentCoveragePercent, + String previousDrivingSourceIntervalId, + String nextDrivingSourceIntervalId, + String previousRegistrationKey, + String nextRegistrationKey, + String previousVehicleKey, + String nextVehicleKey +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java index 666d18a..841e35b 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -14,9 +14,11 @@ import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; 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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import java.io.IOException; @@ -100,9 +102,12 @@ public class DriverTimelineReusableProjectionBuilder { List drivingInterruptionIntervals = new ArrayList<>(); List dailyWeeklyRestCandidateIntervals = new ArrayList<>(); + List dailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>(); + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>(); List drivingInterruptionVehicleChangeIntervals = new ArrayList<>(); List vuCardAbsentIntervals = new ArrayList<>(); List potentialHomeOvernightStayIntervals = new ArrayList<>(); + List potentialInVehicleOvernightStayIntervals = new ArrayList<>(); executeWithRuntime( configuration -> { @@ -119,9 +124,12 @@ public class DriverTimelineReusableProjectionBuilder { Map.of( "drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals), "dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals), + "dailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, dailyWeeklyRestCandidateCoverageIntervals), + "unclassifiedDailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, unclassifiedDailyWeeklyRestCandidateCoverageIntervals), "drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals), "vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals), - "potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals) + "potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals), + "potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals) ), runtime -> { if (vehicleUsageIntervals != null) { @@ -146,14 +154,20 @@ public class DriverTimelineReusableProjectionBuilder { return new TachographEsperDrivingDerivedProjectionBundle( sortDrivingInterruptionIntervals(drivingInterruptionIntervals), sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals), + sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals), + sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals), sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), sortVuCardAbsentIntervals(vuCardAbsentIntervals), - sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals) + sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals), + sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals) ); } private TachographEsperDrivingDerivedProjectionBundle emptyBundle() { return new TachographEsperDrivingDerivedProjectionBundle( + List.of(), + List.of(), + List.of(), List.of(), List.of(), List.of(), @@ -364,6 +378,36 @@ public class DriverTimelineReusableProjectionBuilder { } } + private void collectDailyWeeklyRestCandidateCoverageIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + (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"), + (Long) event.get("cardPresentDurationSeconds"), + (Double) event.get("cardPresentCoveragePercent"), + (Long) event.get("unknownDurationSeconds"), + (Double) event.get("unknownCoveragePercent"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + private void collectPotentialHomeOvernightStayIntervalEvents( EventBean[] newData, List target @@ -392,6 +436,34 @@ public class DriverTimelineReusableProjectionBuilder { } } + private void collectPotentialInVehicleOvernightStayIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( + (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"), + (Long) event.get("cardPresentDurationSeconds"), + (Double) event.get("cardPresentCoveragePercent"), + (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 List sortDrivingInterruptionIntervals( List intervals ) { @@ -410,6 +482,15 @@ public class DriverTimelineReusableProjectionBuilder { .toList(); } + private List sortDailyWeeklyRestCandidateCoverageIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) + .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) + .toList(); + } + private List sortPotentialHomeOvernightStayIntervals( List intervals ) { @@ -419,6 +500,15 @@ public class DriverTimelineReusableProjectionBuilder { .toList(); } + private List sortPotentialInVehicleOvernightStayIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) + .toList(); + } + private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE .replace( diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java index 7ae23c6..cf1a0c4 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java @@ -111,7 +111,9 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE eventSource, sourcePackageRef ); - List supportEvents = buildSupportEvents( + List supportEvents = List.of(); + /* + buildSupportEvents( session, timeline.supportEvents(), driverRef, @@ -120,6 +122,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE eventSource, sourcePackageRef ); + */ return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents); } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 6168cae..58c5bf8 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -13,10 +13,12 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvalu 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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; 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.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import java.time.Duration; @@ -209,8 +211,33 @@ public class TachographFileSessionProcessingService { requestedFrom, requestedTo ); + List rawVehicleUsageIntervals = + driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline); + List dailyWeeklyRestCandidateCoverageIntervals = + clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( + derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(), + rawVuCardAbsentIntervals, + rawVehicleUsageIntervals, + requestedFrom, + requestedTo + ); + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals = + clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( + derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(), + rawVuCardAbsentIntervals, + rawVehicleUsageIntervals, + requestedFrom, + requestedTo + ); + List potentialInVehicleOvernightStayIntervals = + clipEsperPotentialInVehicleOvernightStayIntervalEvents( + derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(), + rawVehicleUsageIntervals, + requestedFrom, + requestedTo + ); List vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents( - driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline), + rawVehicleUsageIntervals, requestedFrom, requestedTo ); @@ -233,7 +260,10 @@ public class TachographFileSessionProcessingService { drivingInterruptionIntervals.size(), drivingInterruptionVehicleChangeIntervals.size(), dailyWeeklyRestCandidateIntervals.size(), + dailyWeeklyRestCandidateCoverageIntervals.size(), + unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(), potentialHomeOvernightStayIntervals.size(), + potentialInVehicleOvernightStayIntervals.size(), vehicleUsageIntervals.size(), vuCardAbsentIntervals.size(), activityIntervals, @@ -241,7 +271,10 @@ public class TachographFileSessionProcessingService { drivingInterruptionIntervals, drivingInterruptionVehicleChangeIntervals, dailyWeeklyRestCandidateIntervals, + dailyWeeklyRestCandidateCoverageIntervals, + unclassifiedDailyWeeklyRestCandidateCoverageIntervals, potentialHomeOvernightStayIntervals, + potentialInVehicleOvernightStayIntervals, vehicleUsageIntervals, vuCardAbsentIntervals, esperProjectionNotes() @@ -413,6 +446,67 @@ public class TachographFileSessionProcessingService { .toList(); } + private List clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( + List intervals, + List rawVuCardAbsentIntervals, + List rawVehicleUsageIntervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo + ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } + return intervals.stream() + .map(interval -> { + OffsetDateTime start = max(interval.startedAt(), requestedFrom); + OffsetDateTime end = min(interval.endedAt(), requestedTo); + if (!end.isAfter(start)) { + return null; + } + long durationSeconds = Duration.between(start, end).getSeconds(); + long cardPresentDurationSeconds = overlapVehicleUsageSeconds( + start, + end, + rawVehicleUsageIntervals, + interval.driverKey(), + null + ); + double cardPresentCoveragePercent = durationSeconds == 0L + ? 0.0d + : (cardPresentDurationSeconds * 100.0d) / durationSeconds; + long unknownDurationSeconds = overlapSeconds( + start, + end, + rawVuCardAbsentIntervals, + interval.driverKey() + ); + double unknownCoveragePercent = durationSeconds == 0L + ? 0.0d + : (unknownDurationSeconds * 100.0d) / durationSeconds; + return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + interval.sessionId(), + interval.driverKey(), + start, + end, + durationSeconds, + cardPresentDurationSeconds, + cardPresentCoveragePercent, + unknownDurationSeconds, + unknownCoveragePercent, + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId(), + interval.previousRegistrationKey(), + interval.nextRegistrationKey(), + interval.previousVehicleKey(), + interval.nextVehicleKey() + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) + .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) + .toList(); + } + private List clipEsperPotentialHomeOvernightStayIntervalEvents( List intervals, List rawVuCardAbsentIntervals, @@ -461,6 +555,55 @@ public class TachographFileSessionProcessingService { .toList(); } + private List clipEsperPotentialInVehicleOvernightStayIntervalEvents( + List intervals, + List rawVehicleUsageIntervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo + ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } + return intervals.stream() + .map(interval -> { + OffsetDateTime start = max(interval.startedAt(), requestedFrom); + OffsetDateTime end = min(interval.endedAt(), requestedTo); + if (!end.isAfter(start)) { + return null; + } + long durationSeconds = Duration.between(start, end).getSeconds(); + long cardPresentDurationSeconds = overlapVehicleUsageSeconds( + start, + end, + rawVehicleUsageIntervals, + interval.driverKey(), + interval.previousRegistrationKey() + ); + double cardPresentCoveragePercent = durationSeconds == 0L + ? 0.0d + : (cardPresentDurationSeconds * 100.0d) / durationSeconds; + return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( + interval.sessionId(), + interval.driverKey(), + start, + end, + durationSeconds, + cardPresentDurationSeconds, + cardPresentCoveragePercent, + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId(), + interval.previousRegistrationKey(), + interval.nextRegistrationKey(), + interval.previousVehicleKey(), + interval.nextVehicleKey() + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) + .toList(); + } + private List synthesizeUnknownGaps( List knownIntervals, Duration gapDetectionTolerance @@ -1027,7 +1170,10 @@ public class TachographFileSessionProcessingService { "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.", + "Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and unknown-coverage metrics computed from vehicle-usage and VU card-absent overlap.", + "Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.", + "Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.", + "Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.", "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." @@ -1057,6 +1203,36 @@ public class TachographFileSessionProcessingService { return total; } + private long overlapVehicleUsageSeconds( + OffsetDateTime intervalStart, + OffsetDateTime intervalEnd, + List vehicleUsageIntervals, + String driverKey, + String registrationKey + ) { + if (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty()) { + return 0L; + } + long total = 0L; + for (TachographEsperVehicleUsageIntervalEvent vehicleUsage : vehicleUsageIntervals) { + if (!Objects.equals(driverKey, vehicleUsage.driverKey())) { + continue; + } + if (registrationKey != null && !Objects.equals(registrationKey, vehicleUsage.registrationKey())) { + continue; + } + if (vehicleUsage.endedAt() == null) { + continue; + } + OffsetDateTime overlapStart = max(intervalStart, vehicleUsage.startedAt()); + OffsetDateTime overlapEnd = min(intervalEnd, vehicleUsage.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); } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java index 549b9ff..0c816f1 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java @@ -18,12 +18,16 @@ import java.util.Comparator; import java.util.HexFormat; import java.util.List; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service public class TachographFileSessionService { + private static final Logger log = LoggerFactory.getLogger(TachographFileSessionService.class); + private final EventHubProperties properties; private final TachographFileSessionRepository repository; private final LegalRequirementsClient legalRequirementsClient; @@ -57,8 +61,14 @@ public class TachographFileSessionService { validateFile(file); byte[] fileBytes = file.getBytes(); validateFileBytes(fileBytes); + long uploadStartedAt = System.nanoTime(); LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename()); + long uploadDurationMs = elapsedMillis(uploadStartedAt); + + long parseStartedAt = System.nanoTime(); TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parse(uploadResult.xmlContent()); + long parseDurationMs = elapsedMillis(parseStartedAt); + Instant createdAt = Instant.now(); Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl()); boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName()); @@ -74,9 +84,21 @@ public class TachographFileSessionService { driverCardFile, null ); + long extractStartedAt = System.nanoTime(); TachographFileSession session = driverCardFile ? driverCardExtractionService.extract(parsedXml, metadata, createdAt, expiresAt) : vehicleUnitExtractionService.extract(parsedXml, metadata, createdAt, expiresAt); + long extractDurationMs = elapsedMillis(extractStartedAt); + + log.info( + "Created tachograph file session timing: file='{}', type={}, uploadMs={}, parseMs={}, extractMs={}, sizeBytes={}", + file.getOriginalFilename(), + driverCardFile ? "DRIVER_CARD" : "VEHICLE_UNIT", + uploadDurationMs, + parseDurationMs, + extractDurationMs, + fileBytes.length + ); TachographFileSession saved = repository.save(session); return new CreateTachographFileSessionResponse(toSummary(saved)); } catch (Exception e) { @@ -196,4 +218,8 @@ public class TachographFileSessionService { throw new IllegalStateException("SHA-256 digest is not available.", e); } } + + private long elapsedMillis(long startedAtNanos) { + return (System.nanoTime() - startedAtNanos) / 1_000_000L; + } } diff --git a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl index 15473e2..784276d 100644 --- a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl +++ b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl @@ -52,6 +52,69 @@ create schema DrivingInterruptionVehicleChangeInterval( nextVehicleKey string ); +create schema DrivingInterruptionVehicleNotChangedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageUnknownResolvedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + unknownDurationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageCardResolvedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardPresentDurationSeconds long, + unknownDurationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardPresentDurationSeconds long, + cardPresentCoveragePercent double, + unknownDurationSeconds long, + unknownCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent; create schema VuCardAbsentInterval( @@ -84,6 +147,40 @@ create schema PotentialHomeOvernightStayInterval( nextVehicleKey string ); +create schema PotentialInVehicleOvernightStayInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardPresentDurationSeconds long, + cardPresentCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema UnclassifiedDailyWeeklyRestCandidateCoverageInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardPresentDurationSeconds long, + cardPresentCoveragePercent double, + unknownDurationSeconds long, + unknownCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + insert into SignificantDrivingInterval select sessionId, @@ -138,6 +235,164 @@ from DailyWeeklyRestCandidateInterval( previousRegistrationKey != nextRegistrationKey ); +insert into DrivingInterruptionVehicleNotChangedInterval +select * +from DailyWeeklyRestCandidateInterval( + previousRegistrationKey is not null, + nextRegistrationKey is not null, + previousRegistrationKey = nextRegistrationKey +); + +insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + 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, + 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 DailyWeeklyRestCandidateInterval as c unidirectional, + VuCardAbsentInterval#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.startedAtEpochSecond, + c.endedAtEpochSecond, + c.durationSeconds, + c.previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId, + c.previousRegistrationKey, + c.nextRegistrationKey, + c.previousVehicleKey, + c.nextVehicleKey; + +insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + 0L as unknownDurationSeconds, + 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 DailyWeeklyRestCandidateInterval as c +where not exists ( + select * from VuCardAbsentInterval#keepall as u + where u.driverKey = c.driverKey + and u.startedAtEpochSecond < c.endedAtEpochSecond + and u.endedAtEpochSecond > c.startedAtEpochSecond +); + +insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + sum( + case + when v.startedAtEpochSecond <= c.startedAtEpochSecond and (v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond) + then c.durationSeconds + when v.startedAtEpochSecond <= c.startedAtEpochSecond + then v.endedAtEpochSecond - c.startedAtEpochSecond + when v.endedAtEpochSecond is null or v.endedAtEpochSecond >= c.endedAtEpochSecond + then c.endedAtEpochSecond - v.startedAtEpochSecond + else v.endedAtEpochSecond - v.startedAtEpochSecond + end + ) as cardPresentDurationSeconds, + c.unknownDurationSeconds as unknownDurationSeconds, + 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 DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c unidirectional, + TachographVehicleUsageIntervalInputEvent#keepall as v +where v.driverKey = c.driverKey + and v.startedAtEpochSecond < c.endedAtEpochSecond + and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond) +group by + c.sessionId, + c.driverKey, + c.startedAtEpochSecond, + c.endedAtEpochSecond, + c.durationSeconds, + c.unknownDurationSeconds, + c.previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId, + c.previousRegistrationKey, + c.nextRegistrationKey, + c.previousVehicleKey, + c.nextVehicleKey; + +insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + 0L as cardPresentDurationSeconds, + c.unknownDurationSeconds as unknownDurationSeconds, + 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 DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c +where not exists ( + select * from TachographVehicleUsageIntervalInputEvent#keepall as v + where v.driverKey = c.driverKey + and v.startedAtEpochSecond < c.endedAtEpochSecond + and (v.endedAtEpochSecond is null or v.endedAtEpochSecond > c.startedAtEpochSecond) +); + +insert into DailyWeeklyRestCandidateCoverageInterval +select + sessionId, + driverKey, + startedAtEpochSecond, + endedAtEpochSecond, + durationSeconds, + cardPresentDurationSeconds, + (cardPresentDurationSeconds * 100.0d) / durationSeconds as cardPresentCoveragePercent, + unknownDurationSeconds, + (unknownDurationSeconds * 100.0d) / durationSeconds as unknownCoveragePercent, + previousDrivingSourceIntervalId, + nextDrivingSourceIntervalId, + previousRegistrationKey, + nextRegistrationKey, + previousVehicleKey, + nextVehicleKey +from DailyWeeklyRestCandidateCoverageCardResolvedInterval; + context PerDriver create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; @@ -180,62 +435,71 @@ select c.startedAtEpochSecond as startedAtEpochSecond, c.endedAtEpochSecond as endedAtEpochSecond, 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.unknownDurationSeconds as unknownDurationSeconds, + c.unknownCoveragePercent 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 DrivingInterruptionVehicleChangeInterval as c unidirectional, - VuCardAbsentInterval#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.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; +from DailyWeeklyRestCandidateCoverageInterval as c +where c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey != c.nextRegistrationKey + and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L; + +insert into PotentialInVehicleOvernightStayInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardPresentDurationSeconds as cardPresentDurationSeconds, + c.cardPresentCoveragePercent as cardPresentCoveragePercent, + 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 DailyWeeklyRestCandidateCoverageInterval as c +where c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds; + +insert into UnclassifiedDailyWeeklyRestCandidateCoverageInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardPresentDurationSeconds as cardPresentDurationSeconds, + c.cardPresentCoveragePercent as cardPresentCoveragePercent, + c.unknownDurationSeconds as unknownDurationSeconds, + c.unknownCoveragePercent as unknownCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey +from DailyWeeklyRestCandidateCoverageInterval as c +where not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey != c.nextRegistrationKey + and c.unknownDurationSeconds * 100L >= c.durationSeconds * 95L +) + and not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardPresentDurationSeconds >= c.durationSeconds +); @name('drivingInterruptionIntervals') select * from DrivingInterruptionInterval; @@ -243,6 +507,9 @@ select * from DrivingInterruptionInterval; @name('dailyWeeklyRestCandidateIntervals') select * from DailyWeeklyRestCandidateInterval; +@name('dailyWeeklyRestCandidateCoverageIntervals') +select * from DailyWeeklyRestCandidateCoverageInterval; + @name('drivingInterruptionVehicleChangeIntervals') select * from DrivingInterruptionVehicleChangeInterval; @@ -251,3 +518,9 @@ select * from VuCardAbsentInterval; @name('potentialHomeOvernightStayIntervals') select * from PotentialHomeOvernightStayInterval; + +@name('potentialInVehicleOvernightStayIntervals') +select * from PotentialInVehicleOvernightStayInterval; + +@name('unclassifiedDailyWeeklyRestCandidateCoverageIntervals') +select * from UnclassifiedDailyWeeklyRestCandidateCoverageInterval; diff --git a/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java new file mode 100644 index 0000000..62799ec --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java @@ -0,0 +1,192 @@ +package at.procon.eventhub.processing.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import at.procon.eventhub.dto.DriverRefDto; +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.dto.VehicleRegistrationRefDto; +import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; +import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService; +import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; +import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +class UnifiedRuntimeProcessingControllerTest { + + @Test + void loadsDriverEventsViaRuntimeApi() throws Exception { + UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); + UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) + .build(); + + UUID sessionId = UUID.randomUUID(); + UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + true, + 0 + ); + when(eventAssemblyService.assembleDriverScopedEvents(any())) + .thenReturn(new UnifiedRuntimeEventBundle( + request, + List.of(event("EV-1", "2026-05-01T08:00:00Z")), + List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")), + List.of(), + List.of(event("EV-1", "2026-05-01T08:00:00Z")), + List.of("note") + )); + + mockMvc.perform(post("/api/eventhub/runtime-processing/driver-events") + .contentType("application/json") + .content(""" + { + "sessionId": "%s", + "sourceFamilies": ["TACHOGRAPH_FILE_SESSION"], + "driverKey": "12:123", + "occurredFrom": "2026-05-01T08:00:00Z", + "occurredTo": "2026-05-01T10:00:00Z", + "expandVehicleEvents": true, + "vehicleExpansionPaddingMinutes": 0 + } + """.formatted(sessionId))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.request.sessionId").value(sessionId.toString())) + .andExpect(jsonPath("$.request.driverKey").value("12:123")) + .andExpect(jsonPath("$.discoveredVehicles[0].vin").value("VIN-1")) + .andExpect(jsonPath("$.mergedEvents[0].externalSourceEventId").value("EV-1")); + } + + @Test + void loadsDriverTimelineViaRuntimeApi() throws Exception { + UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); + UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) + .build(); + + when(timelineService.loadDriverTimeline(any())) + .thenReturn(new ResolvedDriverTimeline( + "UNIFIED_EVENT_STREAM", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + List.of(ResolvedVehicleUsageInterval.resolved( + null, + "DRIVER:42", + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "DRIVER_CARD", + List.of("CVU-1") + )), + List.of(new ResolvedActivityInterval( + "ACT-1", + OffsetDateTime.parse("2026-05-01T08:30:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z"), + 1800L, + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "DRIVER_CARD", + List.of("ACT-1"), + false, + false, + "RAW_INTERVAL" + )), + List.of(), + List.of() + )); + + mockMvc.perform(post("/api/eventhub/runtime-processing/driver-timeline") + .contentType("application/json") + .content(""" + { + "tenantKey": "default", + "sourceFamilies": ["TACHOGRAPH_DB", "YELLOWFOX_DB"], + "eventBackend": "SOURCE_DB", + "driverSourceEntityId": "DRIVER:42", + "occurredFrom": "2026-05-01T08:00:00Z", + "occurredTo": "2026-05-01T10:00:00Z", + "expandVehicleEvents": false, + "vehicleExpansionPaddingMinutes": 15 + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sourceKind").value("UNIFIED_EVENT_STREAM")) + .andExpect(jsonPath("$.activityIntervals[0].intervalId").value("ACT-1")) + .andExpect(jsonPath("$.vehicleUsageIntervals[0].intervalId").value("CVU-1")); + } + + @Test + void returnsBadRequestForInvalidRuntimeRequest() throws Exception { + UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); + UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/eventhub/runtime-processing/driver-events") + .contentType("application/json") + .content(""" + { + "sourceFamilies": ["TACHOGRAPH_DB"], + "occurredFrom": "2026-05-01T08:00:00Z", + "occurredTo": "2026-05-01T10:00:00Z" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").exists()); + } + + private EventHubEventDto event(String externalId, String occurredAt) { + return new EventHubEventDto( + UUID.randomUUID(), + externalId, + new DriverRefDto("12:123", null), + new VehicleRefDto("VEH-1", "VIN-1", "VR-1", new VehicleRegistrationRefDto("12", 12, "REG-1")), + OffsetDateTime.parse(occurredAt), + null, + OffsetDateTime.parse(occurredAt), + EventDomain.DRIVER_ACTIVITY, + EventType.DRIVE, + EventLifecycle.START, + null, + null, + null, + null, + null, + false, + null + ); + } +} diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index eff07cc..f52fe8c 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -20,6 +20,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDri 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.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; @@ -85,6 +86,9 @@ class TachographFileSessionControllerTest { 1, 1, 1, + 0, + 1, + 0, 2, 1, List.of(new TachographEsperActivityIntervalEvent( @@ -164,6 +168,24 @@ class TachographFileSessionControllerTest { "VIN-1", "VIN-2" )), + List.of(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + sessionId, + "12:123", + OffsetDateTime.parse("2026-05-12T10:00:00Z"), + OffsetDateTime.parse("2026-05-12T22:00:00Z"), + 43_200L, + 0L, + 0.0d, + 43_200L, + 100.0d, + "ACT-2", + "ACT-3", + "12:REG-1", + "12:REG-2", + "VIN-1", + "VIN-2" + )), + List.of(), List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent( sessionId, "12:123", @@ -179,6 +201,7 @@ class TachographFileSessionControllerTest { "VIN-1", "VIN-2" )), + List.of(), List.of(new TachographEsperVehicleUsageIntervalEvent( sessionId, "12:123", @@ -273,7 +296,10 @@ class TachographFileSessionControllerTest { .andExpect(jsonPath("$.drivingInterruptionIntervalCount").value(1)) .andExpect(jsonPath("$.drivingInterruptionVehicleChangeIntervalCount").value(1)) .andExpect(jsonPath("$.dailyWeeklyRestCandidateIntervalCount").value(1)) + .andExpect(jsonPath("$.dailyWeeklyRestCandidateCoverageIntervalCount").value(1)) + .andExpect(jsonPath("$.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount").value(0)) .andExpect(jsonPath("$.potentialHomeOvernightStayIntervalCount").value(1)) + .andExpect(jsonPath("$.potentialInVehicleOvernightStayIntervalCount").value(0)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2")) diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java index e3f6ffd..fe16a59 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java @@ -7,6 +7,7 @@ import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInter import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; @@ -122,8 +123,99 @@ class DriverTimelineReusableProjectionBuilderTest { assertThat(reusableBundle.drivingInterruptionIntervals()).containsExactlyElementsOf(legacyDrivingInterruptions); assertThat(reusableBundle.dailyWeeklyRestCandidateIntervals()).containsExactlyElementsOf(legacyDailyWeeklyRestCandidates); + assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1); + TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent coverageInterval = + reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0); + assertThat(coverageInterval.unknownCoveragePercent()).isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d)); + assertThat(coverageInterval.cardPresentCoveragePercent()).isEqualTo(0.0d); assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges); assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals); assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).containsExactlyElementsOf(legacyPotentialHomeOvernightStays); + assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).isEmpty(); + } + + @Test + void buildsPotentialInVehicleOvernightStayIntervalsFromVehicleUnchangedCoveredRest() { + 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-02T01:00:00Z"), + 100L, + 260L, + "12:REG-1", + "VIN-1", + "vu-1" + ) + ), + 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-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, 1, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + ResolvedDriverTimeline timeline = legacyBuilder.build(session, driver); + TachographEsperDrivingDerivedProjectionBundle reusableBundle = + reusableBuilder.buildEsperDrivingDerivedProjectionBundle( + session.sessionId(), + driver.driverKey(), + timeline, + 3, + 720 + ); + + assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).isEmpty(); + assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1); + assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()) + .isEqualTo(100.0d); + assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()) + .isEqualTo(0.0d); + assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).isEmpty(); + assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).hasSize(1); + assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).startedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); + assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).endedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); + assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()) + .isEqualTo(100.0d); } } diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index 3737969..ec44e55 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -100,7 +100,10 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(0); assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).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")); @@ -194,7 +197,10 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(0); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).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"); @@ -291,6 +297,9 @@ class TachographFileSessionProcessingServiceTest { assertThat(result.activityIntervalCount()).isEqualTo(3); assertThat(result.drivingIntervalCount()).isEqualTo(2); assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2); assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1); } @@ -373,19 +382,200 @@ class TachographFileSessionProcessingServiceTest { ); assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); 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.dailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(0L); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(0.0d); 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.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); assertThat(result.potentialHomeOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T11:00:00Z")); assertThat(result.potentialHomeOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T23:00:00Z")); assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownDurationSeconds()).isEqualTo(43_200L); assertThat(result.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent()).isEqualTo(100.0d); } + @Test + void returnsPotentialInVehicleOvernightStayIntervalsWhenVehicleUnchangedAndCardRemainsInserted() { + EventHubProperties properties = new EventHubProperties(); + TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); + TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( + repository, + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), + properties + ); + + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-02T01:00:00Z"), + 100L, + 260L, + "12:REG-1", + "VIN-1", + "vu-1" + ) + ), + 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-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, 1, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + repository.save(session); + + TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults( + session.sessionId(), + driver.driverKey(), + new TachographEsperEventsProcessingRequest( + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + 3, + 720 + ) + ); + + assertThat(result.dailyWeeklyRestCandidateIntervalCount()).isEqualTo(1); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(0); + assertThat(result.drivingInterruptionVehicleChangeIntervalCount()).isEqualTo(0); + assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(1); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); + assertThat(result.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownDurationSeconds()).isEqualTo(0L); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentDurationSeconds()).isEqualTo(50_400L); + assertThat(result.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()).isEqualTo(100.0d); + } + + @Test + void returnsUnclassifiedDailyWeeklyRestCandidateCoverageIntervalsWhenRestCandidateFitsNeitherCategory() { + EventHubProperties properties = new EventHubProperties(); + TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); + TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( + repository, + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), + new EventBackedDriverTimelineBuilder( + new IntervalBackedDriverTimelineEventBuilder( + driverTimelineBuilder, + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ) + ), + properties + ); + + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T14:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-01T18: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:30:00Z"), OffsetDateTime.parse("2026-05-02T01: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", 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-01T10:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:30:00Z"), + 3, + 720 + ) + ); + + assertThat(result.dailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); + assertThat(result.potentialHomeOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.potentialInVehicleOvernightStayIntervalCount()).isEqualTo(0); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()).isEqualTo(1); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).startedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z")); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).endedAt()) + .isEqualTo(OffsetDateTime.parse("2026-05-02T00:30:00Z")); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).cardPresentCoveragePercent()) + .isLessThan(95.0d); + assertThat(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()) + .isLessThan(95.0d); + } + @Test void evaluatesOperatingPeriodsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties();