From 927ac3b90325695f2dd26986a97e4ce66f71d293 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Wed, 27 May 2026 08:44:49 +0200 Subject: [PATCH] Finalize neutral working-time projection architecture --- .../DriverWorkingTimeProcessingResultDto.java | 42 +- .../DriverWorkingTimeActivityInterval.java | 44 - ...DriverWorkingTimeVehicleUsageInterval.java | 35 - ...verWorkingTimeDerivedProjectionEngine.java | 411 +--- .../DriverWorkingTimeProcessingCore.java | 831 +++----- ...rWorkingTimeReusableProjectionBuilder.java | 769 ++++++++ ...riverVehicleEvidenceAttachmentService.java | 3 +- ...nifiedRuntimeDerivedProjectionService.java | 29 +- ...hographEsperDriverProcessingResultDto.java | 730 ++++++- ...iverTimelineReusableProjectionBuilder.java | 1736 +---------------- ...hDerivedProjectionCompatibilityFacade.java | 236 +++ .../TachographDriverWorkingTimeAdapter.java | 500 +++++ .../TachographEsperProcessingCore.java | 15 +- ...TachographEsperProcessingInputAdapter.java | 53 +- ...raphEventTimelineReconstructionHelper.java | 194 ++ ...achographFileSessionProcessingService.java | 20 +- ...chographEsperProcessingCoreParityTest.java | 4 +- ...graphFileSessionProcessingServiceTest.java | 16 +- 18 files changed, 2789 insertions(+), 2879 deletions(-) create mode 100644 src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeReusableProjectionBuilder.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDerivedProjectionCompatibilityFacade.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDriverWorkingTimeAdapter.java create mode 100644 src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEventTimelineReconstructionHelper.java diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java index d0e14be..01c2ddf 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java @@ -1,14 +1,12 @@ package at.procon.eventhub.processing.driverworkingtime.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.TachographEsperPotentialInVehicleTripIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeSupportGeoEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -34,19 +32,19 @@ public record DriverWorkingTimeProcessingResultDto( int vehicleUsageIntervalCount, int vuCardAbsentIntervalCount, int supportGeoEventCount, - List activityIntervals, - List drivingIntervals, - List drivingInterruptionIntervals, - List drivingInterruptionVehicleChangeIntervals, - List dailyWeeklyRestCandidateIntervals, - List dailyWeeklyRestCandidateCoverageIntervals, - List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, - List potentialHomeOvernightStayIntervals, - List potentialInVehicleOvernightStayIntervals, - List potentialInVehicleTripIntervals, - List vehicleUsageIntervals, - List vuCardAbsentIntervals, - List supportGeoEvents, + List activityIntervals, + List drivingIntervals, + List drivingInterruptionIntervals, + List drivingInterruptionVehicleChangeIntervals, + List dailyWeeklyRestCandidateIntervals, + List dailyWeeklyRestCandidateCoverageIntervals, + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, + List potentialHomeOvernightStayIntervals, + List potentialInVehicleOvernightStayIntervals, + List potentialInVehicleTripIntervals, + List vehicleUsageIntervals, + List vuCardAbsentIntervals, + List supportGeoEvents, List notes ) { } diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeActivityInterval.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeActivityInterval.java index 89cbf58..3834deb 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeActivityInterval.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeActivityInterval.java @@ -1,6 +1,5 @@ package at.procon.eventhub.processing.driverworkingtime.model; -import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; @@ -62,49 +61,6 @@ public record DriverWorkingTimeActivityInterval( ); } - public static DriverWorkingTimeActivityInterval fromResolved( - UUID sessionId, - String driverKey, - ResolvedActivityInterval interval - ) { - if (interval == null || interval.from() == null || interval.to() == null) { - return null; - } - return new DriverWorkingTimeActivityInterval( - sessionId, - driverKey, - interval.intervalId(), - interval.activityType(), - interval.slot(), - interval.cardStatus(), - interval.drivingStatus(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - firstSourceIntervalId(interval), - lastSourceIntervalId(interval), - interval.from(), - interval.to(), - interval.from().toEpochSecond(), - interval.to().toEpochSecond(), - interval.durationSeconds(), - interval.sourceIntervalIds(), - interval.synthetic(), - interval.clippedToRequestedPeriod(), - interval.level() - ); - } - - private static String firstSourceIntervalId(ResolvedActivityInterval interval) { - return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); - } - - private static String lastSourceIntervalId(ResolvedActivityInterval interval) { - return interval.sourceIntervalIds().isEmpty() - ? interval.intervalId() - : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1); - } - @SuppressWarnings("unchecked") private static List castStringList(Object value) { return value instanceof List list ? (List) list : List.of(); diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeVehicleUsageInterval.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeVehicleUsageInterval.java index 7f812b4..4bb61b6 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeVehicleUsageInterval.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/model/DriverWorkingTimeVehicleUsageInterval.java @@ -1,6 +1,5 @@ package at.procon.eventhub.processing.driverworkingtime.model; -import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; @@ -52,40 +51,6 @@ public record DriverWorkingTimeVehicleUsageInterval( ); } - public static DriverWorkingTimeVehicleUsageInterval fromResolved(ResolvedVehicleUsageInterval interval) { - if (interval == null || interval.from() == null) { - return null; - } - return new DriverWorkingTimeVehicleUsageInterval( - interval.sessionId(), - interval.driverKey(), - interval.intervalId(), - firstSourceIntervalId(interval), - lastSourceIntervalId(interval), - interval.from(), - interval.to(), - interval.from().toEpochSecond(), - interval.to() == null ? null : interval.to().toEpochSecond(), - interval.durationSeconds(), - interval.odometerBeginKm(), - interval.odometerEndKm(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - interval.sourceIntervalIds() - ); - } - - private static String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) { - return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); - } - - private static String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) { - return interval.sourceIntervalIds().isEmpty() - ? interval.intervalId() - : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1); - } - @SuppressWarnings("unchecked") private static List castStringList(Object value) { return value instanceof List list ? (List) list : List.of(); diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeDerivedProjectionEngine.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeDerivedProjectionEngine.java index e245d64..8c9e986 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeDerivedProjectionEngine.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeDerivedProjectionEngine.java @@ -1,426 +1,23 @@ package at.procon.eventhub.processing.driverworkingtime.service; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeGeoEvidenceEvent; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; -import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; -import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; -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.TachographEsperGeoEvidenceEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; -import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder; -import java.time.OffsetDateTime; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.UUID; import org.springframework.stereotype.Service; @Service public class DriverWorkingTimeDerivedProjectionEngine { - private final DriverTimelineReusableProjectionBuilder legacyProjectionBuilder; + private final DriverWorkingTimeReusableProjectionBuilder projectionBuilder; public DriverWorkingTimeDerivedProjectionEngine( - DriverTimelineReusableProjectionBuilder legacyProjectionBuilder + DriverWorkingTimeReusableProjectionBuilder projectionBuilder ) { - this.legacyProjectionBuilder = legacyProjectionBuilder; + this.projectionBuilder = projectionBuilder; } public DriverWorkingTimeDerivedProjectionBundle build(DriverWorkingTimeProcessingInput input) { Objects.requireNonNull(input, "input must not be null"); - ResolvedDriverTimeline timeline = toResolvedTimeline(input); - TachographEsperDrivingDerivedProjectionBundle legacyBundle = - legacyProjectionBuilder.buildEsperDrivingDerivedProjectionBundle( - safeSessionId(input.sessionId()), - input.driverKey(), - timeline, - input.significantDrivingMinutes(), - input.minimumRestPeriodMinutes() - ); - return new DriverWorkingTimeDerivedProjectionBundle( - legacyBundle.drivingInterruptionIntervals().stream().map(this::mapDrivingInterruption).toList(), - legacyBundle.dailyWeeklyRestCandidateIntervals().stream().map(this::mapDrivingInterruption).toList(), - legacyBundle.dailyWeeklyRestCandidateCoverageIntervals().stream().map(this::mapRestCoverage).toList(), - legacyBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().stream().map(this::mapRestCoverage).toList(), - legacyBundle.drivingInterruptionVehicleChangeIntervals().stream().map(this::mapDrivingInterruption).toList(), - legacyBundle.vuCardAbsentIntervals().stream().map(this::mapVuCardAbsent).toList(), - legacyBundle.potentialHomeOvernightStayIntervals().stream().map(this::mapPotentialHome).toList(), - legacyBundle.potentialInVehicleOvernightStayIntervals().stream().map(this::mapPotentialInVehicle).toList(), - legacyBundle.potentialInVehicleTripIntervals().stream().map(this::mapPotentialTrip).toList() - ); - } - - private ResolvedDriverTimeline toResolvedTimeline(DriverWorkingTimeProcessingInput input) { - List activityIntervals = input.activityIntervals().stream() - .map(this::toResolvedActivityInterval) - .filter(Objects::nonNull) - .toList(); - List vehicleUsageIntervals = input.vehicleUsageIntervals().stream() - .map(this::toResolvedVehicleUsageInterval) - .filter(Objects::nonNull) - .toList(); - List supportEvents = input.supportEvidenceEvents().stream() - .map(this::toExtractedSupportEvent) - .filter(Objects::nonNull) - .toList(); - OffsetDateTime loadedFrom = input.loadedFrom() == null - ? earliest(activityIntervals, vehicleUsageIntervals, supportEvents) - : utc(input.loadedFrom()); - OffsetDateTime loadedTo = input.loadedTo() == null - ? latest(activityIntervals, vehicleUsageIntervals, supportEvents) - : utc(input.loadedTo()); - return new ResolvedDriverTimeline( - input.sourceKind(), - loadedFrom, - loadedTo, - vehicleUsageIntervals, - activityIntervals, - supportEvents, - List.of() - ); - } - - private ResolvedActivityInterval toResolvedActivityInterval(DriverWorkingTimeActivityInterval interval) { - if (interval == null || interval.startedAt() == null || interval.endedAt() == null) { - return null; - } - return new ResolvedActivityInterval( - interval.intervalId(), - utc(interval.startedAt()), - utc(interval.endedAt()), - interval.durationSeconds(), - interval.activityType(), - interval.cardSlot(), - interval.cardStatus(), - interval.drivingStatus(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - interval.sourceIntervalIds(), - interval.synthetic(), - interval.clippedToRequestedPeriod(), - interval.level() - ); - } - - private ResolvedVehicleUsageInterval toResolvedVehicleUsageInterval(DriverWorkingTimeVehicleUsageInterval interval) { - if (interval == null || interval.startedAt() == null) { - return null; - } - return new ResolvedVehicleUsageInterval( - safeSessionId(interval.sessionId()), - interval.driverKey(), - interval.intervalId(), - utc(interval.startedAt()), - utc(interval.endedAt()), - interval.durationSeconds(), - interval.odometerBeginKm(), - interval.odometerEndKm(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - interval.sourceIntervalIds() - ); - } - - private ExtractedSupportEvent toExtractedSupportEvent(RuntimeSupportEvidenceEvent supportEvent) { - if (supportEvent == null || supportEvent.occurredAt() == null) { - return null; - } - Object rawRecordPath = supportEvent.rawAttributes().get("rawRecordPath"); - return new ExtractedSupportEvent( - supportEvent.eventId(), - supportEvent.driverKey(), - utc(supportEvent.occurredAt()), - supportEvent.eventDomain(), - supportEvent.eventType(), - supportEvent.lifecycle(), - null, - supportEvent.registrationKey(), - supportEvent.vehicleKey(), - supportEvent.countryCode(), - supportEvent.regionCode(), - supportEvent.countryFrom(), - supportEvent.countryTo(), - supportEvent.operation(), - supportEvent.latitude(), - supportEvent.longitude(), - null, - supportEvent.odometerKm(), - null, - supportEvent.speedKmh(), - supportEvent.maxSpeedKmh(), - rawRecordPath == null ? null : rawRecordPath.toString() - ); - } - - private DriverWorkingTimeDrivingInterruptionInterval mapDrivingInterruption( - TachographEsperDrivingInterruptionIntervalEvent value - ) { - return new DriverWorkingTimeDrivingInterruptionInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey() - ); - } - - private DriverWorkingTimeVuCardAbsentInterval mapVuCardAbsent(TachographEsperVuCardAbsentIntervalEvent value) { - return new DriverWorkingTimeVuCardAbsentInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.previousUsageIntervalId(), - value.nextUsageIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey() - ); - } - - private DriverWorkingTimeRestCoverageInterval mapRestCoverage( - TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent value - ) { - return new DriverWorkingTimeRestCoverageInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - mapGeoEvidence(value.beginGeoEvent()), - mapGeoEvidence(value.endGeoEvent()) - ); - } - - private DriverWorkingTimeRestCoverageInterval mapPotentialHome( - TachographEsperPotentialHomeOvernightStayIntervalEvent value - ) { - return new DriverWorkingTimeRestCoverageInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - mapGeoEvidence(value.beginGeoEvent()), - mapGeoEvidence(value.endGeoEvent()) - ); - } - - private DriverWorkingTimeRestCoverageInterval mapPotentialInVehicle( - TachographEsperPotentialInVehicleOvernightStayIntervalEvent value - ) { - return new DriverWorkingTimeRestCoverageInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - mapGeoEvidence(value.beginGeoEvent()), - mapGeoEvidence(value.endGeoEvent()) - ); - } - - private DriverWorkingTimePotentialInVehicleTripInterval mapPotentialTrip( - TachographEsperPotentialInVehicleTripIntervalEvent value - ) { - return new DriverWorkingTimePotentialInVehicleTripInterval( - value.sessionId(), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.registrationKey(), - value.vehicleKey(), - value.containedPotentialInVehicleOvernightStayIntervalCount(), - value.containedPotentialInVehicleOvernightStayDurationSeconds(), - value.containedCardAbsentDurationSeconds(), - value.firstPotentialInVehicleOvernightStayStartedAt(), - value.lastPotentialInVehicleOvernightStayEndedAt(), - value.firstPreviousDrivingSourceIntervalId(), - value.lastNextDrivingSourceIntervalId(), - value.potentialInVehicleOvernightStayIntervals().stream().map(this::mapPotentialInVehicle).toList() - ); - } - - private DriverWorkingTimeGeoEvidenceEvent mapGeoEvidence(TachographEsperGeoEvidenceEvent value) { - if (value == null) { - return null; - } - return new DriverWorkingTimeGeoEvidenceEvent( - value.eventId(), - value.eventDomain(), - value.occurredAt(), - value.latitude(), - value.longitude(), - value.distanceSeconds(), - value.odometerKm() - ); - } - - private OffsetDateTime earliest( - List activityIntervals, - List vehicleUsageIntervals, - List supportEvents - ) { - OffsetDateTime earliest = null; - for (ResolvedActivityInterval interval : activityIntervals) { - earliest = min(earliest, interval.from()); - } - for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { - earliest = min(earliest, interval.from()); - } - for (ExtractedSupportEvent event : supportEvents) { - earliest = min(earliest, event.occurredAt()); - } - return earliest; - } - - private OffsetDateTime latest( - List activityIntervals, - List vehicleUsageIntervals, - List supportEvents - ) { - OffsetDateTime latest = null; - for (ResolvedActivityInterval interval : activityIntervals) { - latest = max(latest, interval.to()); - } - for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { - latest = max(latest, interval.to()); - } - for (ExtractedSupportEvent event : supportEvents) { - latest = max(latest, event.occurredAt()); - } - return latest; - } - - private UUID safeSessionId(UUID sessionId) { - return sessionId == null ? new UUID(0L, 0L) : sessionId; - } - - private OffsetDateTime utc(OffsetDateTime value) { - return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); - } - - private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) { - if (left == null) { - return right; - } - if (right == null) { - return left; - } - return left.isAfter(right) ? left : right; - } - - private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) { - if (left == null) { - return right; - } - if (right == null) { - return left; - } - return left.isBefore(right) ? left : right; + return projectionBuilder.buildDerivedProjectionBundle(input); } } diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java index d97ae58..7491f2e 100644 --- a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java @@ -8,17 +8,17 @@ import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeGe import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeSupportGeoEvent; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; -import at.procon.eventhub.tachographfilesession.model.*; import java.time.Duration; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.UUID; import org.springframework.stereotype.Service; /** @@ -51,87 +51,81 @@ public class DriverWorkingTimeProcessingCore { throw new IllegalArgumentException("occurredTo must not be before occurredFrom."); } - List activityIntervals = clipEsperActivityIntervalEvents( - buildEsperActivityIntervalEvents(input.activityIntervals()), + List activityIntervals = clipActivityIntervals( + input.activityIntervals(), requestedFrom, requestedTo ); - List drivingIntervals = clipEsperActivityIntervalEvents( - buildEsperDrivingIntervalEvents(input.activityIntervals()), + List drivingIntervals = clipActivityIntervals( + safeList(input.activityIntervals()).stream() + .filter(interval -> Objects.equals("DRIVE", interval.activityType())) + .toList(), requestedFrom, requestedTo ); DriverWorkingTimeDerivedProjectionBundle derivedProjectionBundle = derivedProjectionEngine.build(input); - List rawDrivingInterruptionIntervals = - derivedProjectionBundle.drivingInterruptionIntervals().stream().map(this::toLegacy).toList(); - List drivingInterruptionIntervals = - clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionIntervals, requestedFrom, requestedTo); - List rawDailyWeeklyRestCandidateIntervals = - derivedProjectionBundle.dailyWeeklyRestCandidateIntervals().stream().map(this::toLegacy).toList(); - List dailyWeeklyRestCandidateIntervals = - clipEsperDrivingInterruptionIntervalEvents(rawDailyWeeklyRestCandidateIntervals, requestedFrom, requestedTo); - List rawDrivingInterruptionVehicleChangeIntervals = - derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals().stream().map(this::toLegacy).toList(); - List drivingInterruptionVehicleChangeIntervals = - clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionVehicleChangeIntervals, requestedFrom, requestedTo); - - List rawVehicleUsageIntervals = - buildEsperVehicleUsageIntervalEvents(input.vehicleUsageIntervals()); - List rawVuCardAbsentIntervals = - derivedProjectionBundle.vuCardAbsentIntervals().stream().map(this::toLegacy).toList(); - - List potentialHomeOvernightStayIntervals = - clipEsperPotentialHomeOvernightStayIntervalEvents( - derivedProjectionBundle.potentialHomeOvernightStayIntervals().stream().map(this::toLegacyPotentialHome).toList(), - rawVuCardAbsentIntervals, - rawVehicleUsageIntervals, + List drivingInterruptionIntervals = + clipDrivingInterruptionIntervals( + derivedProjectionBundle.drivingInterruptionIntervals(), requestedFrom, requestedTo ); - List dailyWeeklyRestCandidateCoverageIntervals = - clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( - derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals().stream().map(this::toLegacyCoverage).toList(), - rawVuCardAbsentIntervals, - rawVehicleUsageIntervals, + List dailyWeeklyRestCandidateIntervals = + clipDrivingInterruptionIntervals( + derivedProjectionBundle.dailyWeeklyRestCandidateIntervals(), requestedFrom, requestedTo ); - List unclassifiedDailyWeeklyRestCandidateCoverageIntervals = - clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( - derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals().stream().map(this::toLegacyCoverage).toList(), - rawVuCardAbsentIntervals, - rawVehicleUsageIntervals, + List drivingInterruptionVehicleChangeIntervals = + clipDrivingInterruptionIntervals( + derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals(), requestedFrom, requestedTo ); - List potentialInVehicleOvernightStayIntervals = - clipEsperPotentialInVehicleOvernightStayIntervalEvents( - derivedProjectionBundle.potentialInVehicleOvernightStayIntervals().stream().map(this::toLegacyPotentialInVehicle).toList(), - rawVuCardAbsentIntervals, - rawVehicleUsageIntervals, + List dailyWeeklyRestCandidateCoverageIntervals = + clipRestCoverageIntervals( + derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(), requestedFrom, requestedTo ); - List potentialInVehicleTripIntervals = - clipEsperPotentialInVehicleTripIntervalEvents( - derivedProjectionBundle.potentialInVehicleTripIntervals().stream().map(this::toLegacy).toList(), + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals = + clipRestCoverageIntervals( + derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(), + requestedFrom, + requestedTo + ); + List potentialHomeOvernightStayIntervals = + clipRestCoverageIntervals( + derivedProjectionBundle.potentialHomeOvernightStayIntervals(), + requestedFrom, + requestedTo + ); + List potentialInVehicleOvernightStayIntervals = + clipRestCoverageIntervals( + derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(), + requestedFrom, + requestedTo + ); + List potentialInVehicleTripIntervals = + clipPotentialInVehicleTripIntervals( + derivedProjectionBundle.potentialInVehicleTripIntervals(), potentialInVehicleOvernightStayIntervals, requestedFrom, requestedTo ); - List vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents( - rawVehicleUsageIntervals, + List vehicleUsageIntervals = clipVehicleUsageIntervals( + input.vehicleUsageIntervals(), requestedFrom, requestedTo ); - List vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents( - rawVuCardAbsentIntervals, + List vuCardAbsentIntervals = clipVuCardAbsentIntervals( + derivedProjectionBundle.vuCardAbsentIntervals(), requestedFrom, requestedTo ); - List supportGeoEvents = clipEsperSupportGeoEvents( + List supportGeoEvents = clipSupportGeoEvents( buildSupportGeoEvents(input.supportEvidenceEvents()), driverKey, requestedFrom, @@ -180,73 +174,106 @@ public class DriverWorkingTimeProcessingCore { return process(input); } - private List buildEsperActivityIntervalEvents( - List intervals + private List clipActivityIntervals( + List intervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } return safeList(intervals).stream() .filter(interval -> interval.startedAt() != null && interval.endedAt() != null) - .map(interval -> new TachographEsperActivityIntervalEvent( - safeSessionId(interval.sessionId()), - interval.driverKey(), - interval.intervalId(), - interval.activityType(), - interval.cardSlot(), - interval.cardStatus(), - interval.drivingStatus(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - utc(interval.startedAt()), - utc(interval.endedAt()), - interval.durationSeconds(), - interval.sourceIntervalIds(), - interval.synthetic(), - interval.clippedToRequestedPeriod(), - interval.level() - )) - .sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt) - .thenComparing(TachographEsperActivityIntervalEvent::endedAt)) + .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 DriverWorkingTimeActivityInterval( + interval.sessionId(), + interval.driverKey(), + interval.intervalId(), + interval.activityType(), + interval.cardSlot(), + interval.cardStatus(), + interval.drivingStatus(), + interval.registrationKey(), + interval.vehicleKey(), + interval.sourceKind(), + interval.firstSourceIntervalId(), + interval.lastSourceIntervalId(), + start, + end, + start.toEpochSecond(), + end.toEpochSecond(), + Duration.between(start, end).getSeconds(), + interval.sourceIntervalIds(), + interval.synthetic(), + clipped, + interval.level() + ); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(DriverWorkingTimeActivityInterval::startedAt) + .thenComparing(DriverWorkingTimeActivityInterval::endedAt) + .thenComparing(DriverWorkingTimeActivityInterval::activityType, Comparator.nullsLast(String::compareTo))) .toList(); } - private List buildEsperDrivingIntervalEvents( - List intervals - ) { - return buildEsperActivityIntervalEvents(intervals).stream() - .filter(interval -> Objects.equals("DRIVE", interval.activityType())) - .toList(); - } - - private List buildEsperVehicleUsageIntervalEvents( - List intervals + private List clipVehicleUsageIntervals( + List intervals, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo ) { + if (requestedFrom == null || requestedTo == null) { + return List.of(); + } return safeList(intervals).stream() - .filter(interval -> interval.startedAt() != null) - .map(interval -> new TachographEsperVehicleUsageIntervalEvent( - safeSessionId(interval.sessionId()), - interval.driverKey(), - interval.intervalId(), - utc(interval.startedAt()), - utc(interval.endedAt()), - interval.durationSeconds(), - interval.odometerBeginKm(), - interval.odometerEndKm(), - interval.registrationKey(), - interval.vehicleKey(), - interval.sourceKind(), - interval.sourceIntervalIds() - )) - .sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt) - .thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))) + .filter(interval -> interval.startedAt() != null && interval.endedAt() != null) + .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 DriverWorkingTimeVehicleUsageInterval( + interval.sessionId(), + interval.driverKey(), + interval.intervalId(), + interval.firstSourceIntervalId(), + interval.lastSourceIntervalId(), + start, + end, + start.toEpochSecond(), + end.toEpochSecond(), + 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(DriverWorkingTimeVehicleUsageInterval::startedAt) + .thenComparing(DriverWorkingTimeVehicleUsageInterval::endedAt) + .thenComparing(DriverWorkingTimeVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo))) .toList(); } - private List buildSupportGeoEvents( + private List buildSupportGeoEvents( List supportEvents ) { return safeList(supportEvents).stream() .filter(event -> event.occurredAt() != null) - .map(event -> new TachographEsperSupportGeoEvent( + .map(event -> new DriverWorkingTimeSupportGeoEvent( event.eventId(), event.driverKey(), utc(event.occurredAt()), @@ -265,8 +292,8 @@ public class DriverWorkingTimeProcessingCore { event.odometerKm(), rawRecordPath(event) )) - .sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt) - .thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo))) + .sorted(Comparator.comparing(DriverWorkingTimeSupportGeoEvent::occurredAt) + .thenComparing(DriverWorkingTimeSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo))) .toList(); } @@ -278,301 +305,8 @@ public class DriverWorkingTimeProcessingCore { return value == null ? null : value.toString(); } - private TachographEsperDrivingInterruptionIntervalEvent toLegacy(DriverWorkingTimeDrivingInterruptionInterval value) { - return new TachographEsperDrivingInterruptionIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey() - ); - } - - private TachographEsperVuCardAbsentIntervalEvent toLegacy(DriverWorkingTimeVuCardAbsentInterval value) { - return new TachographEsperVuCardAbsentIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.previousUsageIntervalId(), - value.nextUsageIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey() - ); - } - - private TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent toLegacyCoverage( - DriverWorkingTimeRestCoverageInterval value - ) { - return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - toLegacy(value.beginGeoEvent()), - toLegacy(value.endGeoEvent()) - ); - } - - private TachographEsperPotentialHomeOvernightStayIntervalEvent toLegacyPotentialHome( - DriverWorkingTimeRestCoverageInterval value - ) { - return new TachographEsperPotentialHomeOvernightStayIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - toLegacy(value.beginGeoEvent()), - toLegacy(value.endGeoEvent()) - ); - } - - private TachographEsperPotentialInVehicleOvernightStayIntervalEvent toLegacyPotentialInVehicle( - DriverWorkingTimeRestCoverageInterval value - ) { - return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.cardAbsentDurationSeconds(), - value.cardAbsentCoveragePercent(), - value.previousDrivingSourceIntervalId(), - value.nextDrivingSourceIntervalId(), - value.previousRegistrationKey(), - value.nextRegistrationKey(), - value.previousVehicleKey(), - value.nextVehicleKey(), - value.beginBoundaryOdometerKm(), - value.endBoundaryOdometerKm(), - value.beginGeoEventId(), - value.beginGeoEventDomain(), - value.beginGeoOccurredAt(), - value.beginLatitude(), - value.beginLongitude(), - value.beginGeoDistanceSeconds(), - value.beginGeoOdometerKm(), - value.endGeoEventId(), - value.endGeoEventDomain(), - value.endGeoOccurredAt(), - value.endLatitude(), - value.endLongitude(), - value.endGeoDistanceSeconds(), - value.endGeoOdometerKm(), - value.geoEvidenceMovementMeters(), - value.geoEvidenceMovementCategory(), - toLegacy(value.beginGeoEvent()), - toLegacy(value.endGeoEvent()) - ); - } - - private TachographEsperPotentialInVehicleTripIntervalEvent toLegacy( - DriverWorkingTimePotentialInVehicleTripInterval value - ) { - return new TachographEsperPotentialInVehicleTripIntervalEvent( - safeSessionId(value.sessionId()), - value.driverKey(), - value.startedAt(), - value.endedAt(), - value.durationSeconds(), - value.registrationKey(), - value.vehicleKey(), - value.containedPotentialInVehicleOvernightStayIntervalCount(), - value.containedPotentialInVehicleOvernightStayDurationSeconds(), - value.containedCardAbsentDurationSeconds(), - value.firstPotentialInVehicleOvernightStayStartedAt(), - value.lastPotentialInVehicleOvernightStayEndedAt(), - value.firstPreviousDrivingSourceIntervalId(), - value.lastNextDrivingSourceIntervalId(), - value.potentialInVehicleOvernightStayIntervals().stream() - .map(this::toLegacyPotentialInVehicle) - .toList() - ); - } - - private TachographEsperGeoEvidenceEvent toLegacy(DriverWorkingTimeGeoEvidenceEvent value) { - if (value == null) { - return null; - } - return new TachographEsperGeoEvidenceEvent( - value.eventId(), - value.eventDomain(), - value.occurredAt(), - value.latitude(), - value.longitude(), - value.distanceSeconds(), - value.odometerKm() - ); - } - - private UUID safeSessionId(UUID sessionId) { - return sessionId == null ? new UUID(0L, 0L) : sessionId; - } - - private List safeList(List values) { - return values == null ? List.of() : values; - } - - private List combinedNotes(List extraNotes) { - List notes = new ArrayList<>(); - notes.addAll(esperProjectionNotes()); - if (extraNotes != null) { - notes.addAll(extraNotes); - } - return List.copyOf(notes); - } - - - private List clipEsperActivityIntervalEvents( - List 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 clipEsperVehicleUsageIntervalEvents( - List 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 clipEsperSupportGeoEvents( - List supportEvents, + private List clipSupportGeoEvents( + List supportEvents, String driverKey, OffsetDateTime requestedFrom, OffsetDateTime requestedTo @@ -585,28 +319,28 @@ public class DriverWorkingTimeProcessingCore { .filter(event -> event.occurredAt() != null) .filter(event -> event.latitude() != null && event.longitude() != null) .filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo)) - .sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt) - .thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo)) - .thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo))) + .sorted(Comparator.comparing(DriverWorkingTimeSupportGeoEvent::occurredAt) + .thenComparing(DriverWorkingTimeSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo)) + .thenComparing(DriverWorkingTimeSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo))) .toList(); } - private List clipEsperDrivingInterruptionIntervalEvents( - List intervals, + private List clipDrivingInterruptionIntervals( + List intervals, OffsetDateTime requestedFrom, OffsetDateTime requestedTo ) { if (requestedFrom == null || requestedTo == null) { return List.of(); } - return intervals.stream() + return safeList(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( + return new DriverWorkingTimeDrivingInterruptionInterval( interval.sessionId(), interval.driverKey(), start, @@ -621,27 +355,27 @@ public class DriverWorkingTimeProcessingCore { ); }) .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt) - .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt)) + .sorted(Comparator.comparing(DriverWorkingTimeDrivingInterruptionInterval::startedAt) + .thenComparing(DriverWorkingTimeDrivingInterruptionInterval::endedAt)) .toList(); } - private List clipEsperVuCardAbsentIntervalEvents( - List intervals, + private List clipVuCardAbsentIntervals( + List intervals, OffsetDateTime requestedFrom, OffsetDateTime requestedTo ) { if (requestedFrom == null || requestedTo == null) { return List.of(); } - return intervals.stream() + return safeList(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( + return new DriverWorkingTimeVuCardAbsentInterval( interval.sessionId(), interval.driverKey(), start, @@ -656,22 +390,20 @@ public class DriverWorkingTimeProcessingCore { ); }) .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt) - .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt)) + .sorted(Comparator.comparing(DriverWorkingTimeVuCardAbsentInterval::startedAt) + .thenComparing(DriverWorkingTimeVuCardAbsentInterval::endedAt)) .toList(); } - private List clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents( - List intervals, - List rawVuCardAbsentIntervals, - List rawVehicleUsageIntervals, + private List clipRestCoverageIntervals( + List intervals, OffsetDateTime requestedFrom, OffsetDateTime requestedTo ) { if (requestedFrom == null || requestedTo == null) { return List.of(); } - return intervals.stream() + return safeList(intervals).stream() .map(interval -> { OffsetDateTime start = max(interval.startedAt(), requestedFrom); OffsetDateTime end = min(interval.endedAt(), requestedTo); @@ -681,7 +413,7 @@ public class DriverWorkingTimeProcessingCore { long durationSeconds = Duration.between(start, end).getSeconds(); boolean beginBoundaryChanged = !start.equals(interval.startedAt()); boolean endBoundaryChanged = !end.equals(interval.endedAt()); - return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + return new DriverWorkingTimeRestCoverageInterval( interval.sessionId(), interval.driverKey(), start, @@ -713,143 +445,39 @@ public class DriverWorkingTimeProcessingCore { endBoundaryChanged ? null : interval.endGeoOdometerKm(), beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(), beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(), - beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()), - endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm()) + beginBoundaryChanged + ? null + : geoEvidenceEvent( + interval.beginGeoEventId(), + interval.beginGeoEventDomain(), + interval.beginGeoOccurredAt(), + interval.beginLatitude(), + interval.beginLongitude(), + interval.beginGeoDistanceSeconds(), + interval.beginGeoOdometerKm() + ), + endBoundaryChanged + ? null + : geoEvidenceEvent( + interval.endGeoEventId(), + interval.endGeoEventDomain(), + interval.endGeoOccurredAt(), + interval.endLatitude(), + interval.endLongitude(), + interval.endGeoDistanceSeconds(), + interval.endGeoOdometerKm() + ) ); }) .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) - .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) + .sorted(Comparator.comparing(DriverWorkingTimeRestCoverageInterval::startedAt) + .thenComparing(DriverWorkingTimeRestCoverageInterval::endedAt)) .toList(); } - private List clipEsperPotentialHomeOvernightStayIntervalEvents( - 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(); - boolean beginBoundaryChanged = !start.equals(interval.startedAt()); - boolean endBoundaryChanged = !end.equals(interval.endedAt()); - return new TachographEsperPotentialHomeOvernightStayIntervalEvent( - interval.sessionId(), - interval.driverKey(), - start, - end, - durationSeconds, - interval.cardAbsentDurationSeconds(), - interval.cardAbsentCoveragePercent(), - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId(), - interval.previousRegistrationKey(), - interval.nextRegistrationKey(), - interval.previousVehicleKey(), - interval.nextVehicleKey(), - beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(), - endBoundaryChanged ? null : interval.endBoundaryOdometerKm(), - beginBoundaryChanged ? null : interval.beginGeoEventId(), - beginBoundaryChanged ? null : interval.beginGeoEventDomain(), - beginBoundaryChanged ? null : interval.beginGeoOccurredAt(), - beginBoundaryChanged ? null : interval.beginLatitude(), - beginBoundaryChanged ? null : interval.beginLongitude(), - beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(), - beginBoundaryChanged ? null : interval.beginGeoOdometerKm(), - endBoundaryChanged ? null : interval.endGeoEventId(), - endBoundaryChanged ? null : interval.endGeoEventDomain(), - endBoundaryChanged ? null : interval.endGeoOccurredAt(), - endBoundaryChanged ? null : interval.endLatitude(), - endBoundaryChanged ? null : interval.endLongitude(), - endBoundaryChanged ? null : interval.endGeoDistanceSeconds(), - endBoundaryChanged ? null : interval.endGeoOdometerKm(), - beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(), - beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(), - beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()), - endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm()) - ); - }) - .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) - .toList(); - } - - private List clipEsperPotentialInVehicleOvernightStayIntervalEvents( - List intervals, - List rawVuCardAbsentIntervals, - List rawVehicleUsageIntervals, - OffsetDateTime requestedFrom, - OffsetDateTime requestedTo - ) { - if (requestedFrom == null || requestedTo == null) { - return List.of(); - } - return intervals.stream() - .map(interval -> { - OffsetDateTime start = max(interval.startedAt(), requestedFrom); - OffsetDateTime end = min(interval.endedAt(), requestedTo); - if (!end.isAfter(start)) { - return null; - } - long durationSeconds = Duration.between(start, end).getSeconds(); - boolean beginBoundaryChanged = !start.equals(interval.startedAt()); - boolean endBoundaryChanged = !end.equals(interval.endedAt()); - return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( - interval.sessionId(), - interval.driverKey(), - start, - end, - durationSeconds, - interval.cardAbsentDurationSeconds(), - interval.cardAbsentCoveragePercent(), - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId(), - interval.previousRegistrationKey(), - interval.nextRegistrationKey(), - interval.previousVehicleKey(), - interval.nextVehicleKey(), - beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(), - endBoundaryChanged ? null : interval.endBoundaryOdometerKm(), - beginBoundaryChanged ? null : interval.beginGeoEventId(), - beginBoundaryChanged ? null : interval.beginGeoEventDomain(), - beginBoundaryChanged ? null : interval.beginGeoOccurredAt(), - beginBoundaryChanged ? null : interval.beginLatitude(), - beginBoundaryChanged ? null : interval.beginLongitude(), - beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(), - beginBoundaryChanged ? null : interval.beginGeoOdometerKm(), - endBoundaryChanged ? null : interval.endGeoEventId(), - endBoundaryChanged ? null : interval.endGeoEventDomain(), - endBoundaryChanged ? null : interval.endGeoOccurredAt(), - endBoundaryChanged ? null : interval.endLatitude(), - endBoundaryChanged ? null : interval.endLongitude(), - endBoundaryChanged ? null : interval.endGeoDistanceSeconds(), - endBoundaryChanged ? null : interval.endGeoOdometerKm(), - beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(), - beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(), - beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()), - endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm()) - ); - }) - .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) - .toList(); - } - - private List clipEsperPotentialInVehicleTripIntervalEvents( - List intervals, - List potentialInVehicleOvernightStayIntervals, + private List clipPotentialInVehicleTripIntervals( + List intervals, + List potentialInVehicleOvernightStayIntervals, OffsetDateTime requestedFrom, OffsetDateTime requestedTo ) { @@ -867,7 +495,7 @@ public class DriverWorkingTimeProcessingCore { if (!end.isAfter(start)) { return null; } - List containedIntervals = + List containedIntervals = potentialInVehicleOvernightStayIntervals.stream() .filter(candidate -> tripContainsPotentialInterval( interval.driverKey(), @@ -877,16 +505,15 @@ public class DriverWorkingTimeProcessingCore { end, candidate )) - .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) + .sorted(Comparator.comparing(DriverWorkingTimeRestCoverageInterval::startedAt) + .thenComparing(DriverWorkingTimeRestCoverageInterval::endedAt)) .toList(); if (containedIntervals.isEmpty()) { return null; } - TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0); - TachographEsperPotentialInVehicleOvernightStayIntervalEvent last = - containedIntervals.get(containedIntervals.size() - 1); - return new TachographEsperPotentialInVehicleTripIntervalEvent( + DriverWorkingTimeRestCoverageInterval first = containedIntervals.getFirst(); + DriverWorkingTimeRestCoverageInterval last = containedIntervals.getLast(); + return new DriverWorkingTimePotentialInVehicleTripInterval( interval.sessionId(), interval.driverKey(), start, @@ -896,10 +523,10 @@ public class DriverWorkingTimeProcessingCore { interval.vehicleKey(), containedIntervals.size(), containedIntervals.stream() - .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds) + .mapToLong(DriverWorkingTimeRestCoverageInterval::durationSeconds) .sum(), containedIntervals.stream() - .mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds) + .mapToLong(DriverWorkingTimeRestCoverageInterval::cardAbsentDurationSeconds) .sum(), first.startedAt(), last.endedAt(), @@ -909,8 +536,8 @@ public class DriverWorkingTimeProcessingCore { ); }) .filter(Objects::nonNull) - .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt)) + .sorted(Comparator.comparing(DriverWorkingTimePotentialInVehicleTripInterval::startedAt) + .thenComparing(DriverWorkingTimePotentialInVehicleTripInterval::endedAt)) .toList(); } @@ -920,7 +547,7 @@ public class DriverWorkingTimeProcessingCore { String vehicleKey, OffsetDateTime tripStartedAt, OffsetDateTime tripEndedAt, - TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate + DriverWorkingTimeRestCoverageInterval candidate ) { if (!Objects.equals(driverKey, candidate.driverKey())) { return false; @@ -936,10 +563,51 @@ public class DriverWorkingTimeProcessingCore { && !candidate.endedAt().isAfter(tripEndedAt); } + private DriverWorkingTimeGeoEvidenceEvent geoEvidenceEvent( + String eventId, + String eventDomain, + OffsetDateTime occurredAt, + Double latitude, + Double longitude, + Long distanceSeconds, + Long odometerKm + ) { + if (eventId == null + && eventDomain == null + && occurredAt == null + && latitude == null + && longitude == null + && distanceSeconds == null + && odometerKm == null) { + return null; + } + return new DriverWorkingTimeGeoEvidenceEvent( + eventId, + eventDomain, + occurredAt, + latitude, + longitude, + distanceSeconds, + odometerKm + ); + } + + private List safeList(List values) { + return values == null ? List.of() : values; + } + + private List combinedNotes(List extraNotes) { + List notes = new ArrayList<>(); + notes.addAll(esperProjectionNotes()); + if (extraNotes != null) { + notes.addAll(extraNotes); + } + return List.copyOf(notes); + } private List esperProjectionNotes() { return List.of( - "This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.", + "This result returns Esper-backed per-driver interval projections from the common runtime working-time engine.", "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.", @@ -959,36 +627,7 @@ public class DriverWorkingTimeProcessingCore { } private OffsetDateTime utc(OffsetDateTime value) { - return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); - } - - private TachographEsperGeoEvidenceEvent geoEvidenceEvent( - String eventId, - String eventDomain, - OffsetDateTime occurredAt, - Double latitude, - Double longitude, - Long distanceSeconds, - Long odometerKm - ) { - if (eventId == null - && eventDomain == null - && occurredAt == null - && latitude == null - && longitude == null - && distanceSeconds == null - && odometerKm == null) { - return null; - } - return new TachographEsperGeoEvidenceEvent( - eventId, - eventDomain, - occurredAt, - latitude, - longitude, - distanceSeconds, - odometerKm - ); + return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC); } private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) { diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeReusableProjectionBuilder.java new file mode 100644 index 0000000..2154669 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeReusableProjectionBuilder.java @@ -0,0 +1,769 @@ +package at.procon.eventhub.processing.driverworkingtime.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.config.EventHubProperties; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeGeoEvidenceEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; +import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; +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.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.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.StreamUtils; + +@Service +public class DriverWorkingTimeReusableProjectionBuilder { + + private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); + private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE = + loadResource("esper/driver-working-time-derived-projections.epl"); + + private final EventHubProperties properties; + + @Autowired + public DriverWorkingTimeReusableProjectionBuilder( + EventHubProperties properties + ) { + this.properties = properties; + } + + public DriverWorkingTimeDerivedProjectionBundle buildDerivedProjectionBundle( + DriverWorkingTimeProcessingInput input + ) { + if (input == null) { + return emptyDerivedProjectionBundle(); + } + return buildDerivedProjectionBundle( + buildActivityIntervalInputEvents(input.activityIntervals()), + buildVehicleUsageIntervalInputEventsCommon(input.vehicleUsageIntervals()), + buildSupportGeoInputEventsCommon(input.sessionId(), input.supportEvidenceEvents()), + input.significantDrivingMinutes(), + input.minimumRestPeriodMinutes() + ); + } + + private DriverWorkingTimeDerivedProjectionBundle buildDerivedProjectionBundle( + List> activityInputEvents, + List> vehicleUsageInputEvents, + List> supportGeoInputEvents, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if ((activityInputEvents == null || activityInputEvents.isEmpty()) + && (vehicleUsageInputEvents == null || vehicleUsageInputEvents.isEmpty())) { + return emptyDerivedProjectionBundle(); + } + + 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<>(); + List potentialInVehicleTripIntervals = new ArrayList<>(); + + executeWithRuntime( + configuration -> { + configuration.getCommon().addEventType( + "TachographActivityIntervalInputEvent", + activityIntervalInputDefinition() + ); + configuration.getCommon().addEventType( + "TachographVehicleUsageIntervalInputEvent", + vehicleUsageIntervalInputDefinition() + ); + configuration.getCommon().addEventType( + "TachographSupportGeoEvidenceInputEvent", + supportGeoEvidenceInputDefinition() + ); + }, + renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes), + Map.of( + "drivingInterruptionIntervals", + newData -> collectDrivingInterruptionIntervalModels(newData, drivingInterruptionIntervals), + "dailyWeeklyRestCandidateIntervals", + newData -> collectDrivingInterruptionIntervalModels(newData, dailyWeeklyRestCandidateIntervals), + "dailyWeeklyRestCandidateCoverageIntervals", + newData -> collectDailyWeeklyRestCandidateCoverageIntervalModels(newData, dailyWeeklyRestCandidateCoverageIntervals), + "unclassifiedDailyWeeklyRestCandidateCoverageIntervals", + newData -> collectDailyWeeklyRestCandidateCoverageIntervalModels(newData, unclassifiedDailyWeeklyRestCandidateCoverageIntervals), + "drivingInterruptionVehicleChangeIntervals", + newData -> collectDrivingInterruptionIntervalModels(newData, drivingInterruptionVehicleChangeIntervals), + "vuCardAbsentIntervals", + newData -> collectVuCardAbsentIntervalModels(newData, vuCardAbsentIntervals), + "potentialHomeOvernightStayIntervals", + newData -> collectPotentialHomeOvernightStayIntervalModels(newData, potentialHomeOvernightStayIntervals), + "potentialInVehicleOvernightStayIntervals", + newData -> collectPotentialInVehicleOvernightStayIntervalModels(newData, potentialInVehicleOvernightStayIntervals), + "potentialInVehicleTripIntervals", + newData -> collectPotentialInVehicleTripIntervalModels(newData, potentialInVehicleTripIntervals) + ), + runtime -> { + if (supportGeoInputEvents != null) { + for (Map supportGeoEvidence : supportGeoInputEvents) { + runtime.getEventService().sendEventMap( + supportGeoEvidence, + "TachographSupportGeoEvidenceInputEvent" + ); + } + } + if (vehicleUsageInputEvents != null) { + for (Map interval : vehicleUsageInputEvents) { + runtime.getEventService().sendEventMap( + interval, + "TachographVehicleUsageIntervalInputEvent" + ); + } + } + if (activityInputEvents != null) { + for (Map interval : activityInputEvents) { + runtime.getEventService().sendEventMap( + interval, + "TachographActivityIntervalInputEvent" + ); + } + } + } + ); + + List sortedDailyWeeklyRestCandidateCoverageIntervals = + sortDailyWeeklyRestCandidateCoverageIntervalsCommon(dailyWeeklyRestCandidateCoverageIntervals); + List sortedUnclassifiedDailyWeeklyRestCandidateCoverageIntervals = + sortDailyWeeklyRestCandidateCoverageIntervalsCommon(unclassifiedDailyWeeklyRestCandidateCoverageIntervals); + List sortedPotentialHomeOvernightStayIntervals = + sortPotentialHomeOvernightStayIntervalsCommon(potentialHomeOvernightStayIntervals); + List sortedPotentialInVehicleOvernightStayIntervals = + sortPotentialInVehicleOvernightStayIntervalsCommon(potentialInVehicleOvernightStayIntervals); + + return new DriverWorkingTimeDerivedProjectionBundle( + sortDrivingInterruptionIntervalsCommon(drivingInterruptionIntervals), + sortDrivingInterruptionIntervalsCommon(dailyWeeklyRestCandidateIntervals), + sortedDailyWeeklyRestCandidateCoverageIntervals, + sortedUnclassifiedDailyWeeklyRestCandidateCoverageIntervals, + sortDrivingInterruptionIntervalsCommon(drivingInterruptionVehicleChangeIntervals), + sortVuCardAbsentIntervalsCommon(vuCardAbsentIntervals), + sortedPotentialHomeOvernightStayIntervals, + sortedPotentialInVehicleOvernightStayIntervals, + sortPotentialInVehicleTripIntervalsCommon(potentialInVehicleTripIntervals) + ); + } + + private List> buildActivityIntervalInputEvents( + List activityIntervals + ) { + return safeList(activityIntervals).stream() + .map(this::toActivityIntervalInputMap) + .filter(Objects::nonNull) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) + .toList(); + } + + private List> buildVehicleUsageIntervalInputEventsCommon( + List vehicleUsageIntervals + ) { + return safeList(vehicleUsageIntervals).stream() + .map(this::toVehicleUsageIntervalInputMap) + .filter(Objects::nonNull) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) + .toList(); + } + + private List> buildSupportGeoInputEventsCommon( + UUID sessionId, + List supportEvents + ) { + return safeList(supportEvents).stream() + .map(event -> toSupportGeoEvidenceInputMap(sessionId, event)) + .filter(Objects::nonNull) + .sorted(Comparator + .comparing((Map event) -> (Long) event.get("occurredAtEpochSecond")) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("eventId"), ""))) + .toList(); + } + + private List safeList(List values) { + return values == null ? List.of() : values; + } + + private DriverWorkingTimeDerivedProjectionBundle emptyDerivedProjectionBundle() { + return new DriverWorkingTimeDerivedProjectionBundle( + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + ); + } + + private void executeWithRuntime( + Consumer configurationSetup, + String epl, + Map> listeners, + Consumer sender + ) { + EPRuntime runtime = null; + try { + Configuration configuration = new Configuration(); + configurationSetup.accept(configuration); + String runtimeUri = "eventhub-tachograph-reusable-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); + for (Map.Entry> entry : listeners.entrySet()) { + runtime.getDeploymentService() + .getStatement(deployment.getDeploymentId(), entry.getKey()) + .addListener((newData, oldData, statement, rt) -> entry.getValue().accept(newData)); + } + + sender.accept(runtime); + } catch (EPCompileException | EPDeployException e) { + throw new IllegalStateException("Cannot compile/deploy reusable tachograph projection EPL bundle", e); + } finally { + if (runtime != null) { + runtime.destroy(); + } + } + } + + private Map activityIntervalInputDefinition() { + Map 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 vehicleUsageIntervalInputDefinition() { + Map 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 supportGeoEvidenceInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("eventId", String.class); + definition.put("eventDomain", String.class); + definition.put("occurredAt", OffsetDateTime.class); + definition.put("occurredAtEpochSecond", long.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("latitude", Double.class); + definition.put("longitude", Double.class); + definition.put("odometerKm", Long.class); + definition.put("priority", int.class); + return definition; + } + + private Map toActivityIntervalInputMap( + DriverWorkingTimeActivityInterval interval + ) { + if (interval == null || interval.startedAt() == null || interval.endedAt() == null) { + return null; + } + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("intervalId", interval.intervalId()); + event.put("activityType", interval.activityType()); + event.put("cardSlot", interval.cardSlot()); + 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", interval.firstSourceIntervalId()); + event.put("lastSourceIntervalId", interval.lastSourceIntervalId()); + event.put("startedAt", interval.startedAt()); + event.put("endedAt", interval.endedAt()); + event.put("startedAtEpochSecond", interval.startedAtEpochSecond()); + event.put("endedAtEpochSecond", interval.endedAtEpochSecond()); + 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 Map toVehicleUsageIntervalInputMap(DriverWorkingTimeVehicleUsageInterval interval) { + if (interval == null || interval.startedAt() == null) { + return null; + } + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("intervalId", interval.intervalId()); + event.put("firstSourceIntervalId", interval.firstSourceIntervalId()); + event.put("lastSourceIntervalId", interval.lastSourceIntervalId()); + event.put("startedAt", interval.startedAt()); + event.put("endedAt", interval.endedAt()); + event.put("startedAtEpochSecond", interval.startedAtEpochSecond()); + event.put("endedAtEpochSecond", interval.endedAtEpochSecond()); + 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 toSupportGeoEvidenceInputMap( + UUID sessionId, + RuntimeSupportEvidenceEvent supportEvent + ) { + if (supportEvent == null + || supportEvent.driverKey() == null + || supportEvent.occurredAt() == null + || supportEvent.latitude() == null + || supportEvent.longitude() == null) { + return null; + } + int priority = supportGeoPriority(supportEvent.eventDomain()); + if (priority <= 0) { + return null; + } + Map event = new LinkedHashMap<>(); + event.put("sessionId", sessionId == null ? new UUID(0L, 0L) : sessionId); + event.put("driverKey", supportEvent.driverKey()); + event.put("eventId", supportEvent.eventId()); + event.put("eventDomain", supportEvent.eventDomain()); + event.put("occurredAt", supportEvent.occurredAt()); + event.put("occurredAtEpochSecond", supportEvent.occurredAt().toEpochSecond()); + event.put("registrationKey", supportEvent.registrationKey()); + event.put("vehicleKey", supportEvent.vehicleKey()); + event.put("latitude", supportEvent.latitude().doubleValue()); + event.put("longitude", supportEvent.longitude().doubleValue()); + event.put("odometerKm", supportEvent.odometerKm()); + event.put("priority", priority); + return event; + } + + private int supportGeoPriority(String eventDomain) { + if (eventDomain == null || eventDomain.isBlank()) { + return 0; + } + return switch (eventDomain.trim().toUpperCase()) { + case "POSITION" -> 500; + case "PLACE" -> 400; + case "BORDER_CROSSING" -> 300; + case "LOAD_UNLOAD" -> 250; + default -> 0; + }; + } + + private void collectDrivingInterruptionIntervalModels( + 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 DriverWorkingTimeDrivingInterruptionInterval( + (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 collectVuCardAbsentIntervalModels( + 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 DriverWorkingTimeVuCardAbsentInterval( + (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 collectDailyWeeklyRestCandidateCoverageIntervalModels( + EventBean[] newData, + List target + ) { + collectRestCoverageModels(newData, target); + } + + private void collectPotentialHomeOvernightStayIntervalModels( + EventBean[] newData, + List target + ) { + collectRestCoverageModels(newData, target); + } + + private void collectPotentialInVehicleOvernightStayIntervalModels( + EventBean[] newData, + List target + ) { + collectRestCoverageModels(newData, target); + } + + private void collectRestCoverageModels( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + DriverWorkingTimeGeoEvidenceEvent beginGeoEvent = geoEvidenceModel( + (String) event.get("beginGeoEventId"), + (String) event.get("beginGeoEventDomain"), + offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), + (Double) event.get("beginLatitude"), + (Double) event.get("beginLongitude"), + longOrNull(event.get("beginGeoDistanceSeconds")), + longOrNull(event.get("beginGeoOdometerKm")) + ); + DriverWorkingTimeGeoEvidenceEvent endGeoEvent = geoEvidenceModel( + (String) event.get("endGeoEventId"), + (String) event.get("endGeoEventDomain"), + offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), + (Double) event.get("endLatitude"), + (Double) event.get("endLongitude"), + longOrNull(event.get("endGeoDistanceSeconds")), + longOrNull(event.get("endGeoOdometerKm")) + ); + target.add(new DriverWorkingTimeRestCoverageInterval( + (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("cardAbsentDurationSeconds"), + (Double) event.get("cardAbsentCoveragePercent"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey"), + longOrNull(event.get("beginBoundaryOdometerKm")), + longOrNull(event.get("endBoundaryOdometerKm")), + (String) event.get("beginGeoEventId"), + (String) event.get("beginGeoEventDomain"), + offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), + (Double) event.get("beginLatitude"), + (Double) event.get("beginLongitude"), + longOrNull(event.get("beginGeoDistanceSeconds")), + longOrNull(event.get("beginGeoOdometerKm")), + (String) event.get("endGeoEventId"), + (String) event.get("endGeoEventDomain"), + offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), + (Double) event.get("endLatitude"), + (Double) event.get("endLongitude"), + longOrNull(event.get("endGeoDistanceSeconds")), + longOrNull(event.get("endGeoOdometerKm")), + longOrNull(event.get("geoEvidenceMovementMeters")), + (String) event.get("geoEvidenceMovementCategory"), + beginGeoEvent, + endGeoEvent + )); + } + } + + private void collectPotentialInVehicleTripIntervalModels( + 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 DriverWorkingTimePotentialInVehicleTripInterval( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), + (Long) event.get("durationSeconds"), + (String) event.get("registrationKey"), + (String) event.get("vehicleKey"), + (Integer) event.get("containedPotentialInVehicleOvernightStayIntervalCount"), + (Long) event.get("containedPotentialInVehicleOvernightStayDurationSeconds"), + (Long) event.get("containedCardAbsentDurationSeconds"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("firstPotentialInVehicleOvernightStayStartedAtEpochSecond")), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("lastPotentialInVehicleOvernightStayEndedAtEpochSecond")), ZoneOffset.UTC), + (String) event.get("firstPreviousDrivingSourceIntervalId"), + (String) event.get("lastNextDrivingSourceIntervalId"), + List.of() + )); + } + } + + private List sortDrivingInterruptionIntervalsCommon( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(DriverWorkingTimeDrivingInterruptionInterval::startedAt) + .thenComparing(DriverWorkingTimeDrivingInterruptionInterval::endedAt)) + .toList(); + } + + private List sortVuCardAbsentIntervalsCommon( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(DriverWorkingTimeVuCardAbsentInterval::startedAt) + .thenComparing(DriverWorkingTimeVuCardAbsentInterval::endedAt)) + .toList(); + } + + private List sortDailyWeeklyRestCandidateCoverageIntervalsCommon( + List intervals + ) { + return deduplicateRestCoverageIntervalsCommon(intervals).stream() + .sorted(Comparator.comparing(DriverWorkingTimeRestCoverageInterval::startedAt) + .thenComparing(DriverWorkingTimeRestCoverageInterval::endedAt)) + .toList(); + } + + private List sortPotentialHomeOvernightStayIntervalsCommon( + List intervals + ) { + return deduplicateRestCoverageIntervalsCommon(intervals).stream() + .sorted(Comparator.comparing(DriverWorkingTimeRestCoverageInterval::startedAt) + .thenComparing(DriverWorkingTimeRestCoverageInterval::endedAt)) + .toList(); + } + + private List sortPotentialInVehicleOvernightStayIntervalsCommon( + List intervals + ) { + return deduplicateRestCoverageIntervalsCommon(intervals).stream() + .sorted(Comparator.comparing(DriverWorkingTimeRestCoverageInterval::startedAt) + .thenComparing(DriverWorkingTimeRestCoverageInterval::endedAt)) + .toList(); + } + + private List sortPotentialInVehicleTripIntervalsCommon( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(DriverWorkingTimePotentialInVehicleTripInterval::startedAt) + .thenComparing(DriverWorkingTimePotentialInVehicleTripInterval::endedAt)) + .toList(); + } + + private List deduplicateRestCoverageIntervalsCommon( + List intervals + ) { + Map deduplicated = new LinkedHashMap<>(); + for (DriverWorkingTimeRestCoverageInterval interval : intervals) { + deduplicated.put(restCoverageIntervalKey( + interval.driverKey(), + interval.startedAt(), + interval.endedAt(), + interval.previousDrivingSourceIntervalId(), + interval.nextDrivingSourceIntervalId() + ), interval); + } + return new ArrayList<>(deduplicated.values()); + } + + private String restCoverageIntervalKey( + String driverKey, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + String previousDrivingSourceIntervalId, + String nextDrivingSourceIntervalId + ) { + return String.join("|", + driverKey == null ? "" : driverKey, + startedAt == null ? "" : startedAt.toString(), + endedAt == null ? "" : endedAt.toString(), + previousDrivingSourceIntervalId == null ? "" : previousDrivingSourceIntervalId, + nextDrivingSourceIntervalId == null ? "" : nextDrivingSourceIntervalId + ); + } + + private DriverWorkingTimeGeoEvidenceEvent geoEvidenceModel( + String eventId, + String eventDomain, + OffsetDateTime occurredAt, + Double latitude, + Double longitude, + Long distanceSeconds, + Long odometerKm + ) { + if (eventId == null + && eventDomain == null + && occurredAt == null + && latitude == null + && longitude == null + && distanceSeconds == null + && odometerKm == null) { + return null; + } + return new DriverWorkingTimeGeoEvidenceEvent( + eventId, + eventDomain, + occurredAt, + latitude, + longitude, + distanceSeconds, + odometerKm + ); + } + + private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { + return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE + .replace( + "${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}", + Long.toString(Math.max(1, significantDrivingMinutes) * 60L) + ) + .replace( + "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}", + Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L) + ) + .replace( + "${REST_GEO_LOOKBACK_SECONDS}", + Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookbackMinutes()) * 60L) + ) + .replace( + "${REST_GEO_LOOKAHEAD_SECONDS}", + Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookaheadMinutes()) * 60L) + ) + .replace( + "${REST_GEO_STATIONARY_MAX_METERS}", + Integer.toString(Math.max(0, properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters())) + ) + .replace( + "${REST_GEO_MINOR_MOVEMENT_MAX_METERS}", + Integer.toString(Math.max( + properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters(), + properties.getTachographFileSession().getProcessing().getRestCandidateGeoMinorMovementMaxMeters() + )) + ); + } + + private OffsetDateTime offsetDateTime(Object epochSecond) { + if (!(epochSecond instanceof Long value)) { + return null; + } + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(value), ZoneOffset.UTC); + } + + private Long longOrNull(Object value) { + return value instanceof Long number ? number : null; + } + + 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); + } + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverVehicleEvidenceAttachmentService.java b/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverVehicleEvidenceAttachmentService.java index 6c525e5..6df4b19 100644 --- a/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverVehicleEvidenceAttachmentService.java +++ b/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverVehicleEvidenceAttachmentService.java @@ -10,6 +10,7 @@ import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScope import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.service.TachographDriverWorkingTimeAdapter; import com.fasterxml.jackson.databind.JsonNode; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -67,7 +68,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService { ); List vehicleUsageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals()) .stream() - .map(DriverWorkingTimeVehicleUsageInterval::fromResolved) + .map(TachographDriverWorkingTimeAdapter::toVehicleUsageInterval) .filter(Objects::nonNull) .toList(); return attachVehicleEvidence( diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java index 214a947..1139b7a 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java @@ -10,12 +10,15 @@ import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeAc import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeDerivedProjectionEngine; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeReusableProjectionBuilder; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; +import at.procon.eventhub.tachographfilesession.service.TachographDriverWorkingTimeAdapter; import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; @@ -28,9 +31,6 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialIn import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; -import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder; -import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder; -import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore; import java.time.Duration; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -47,8 +47,6 @@ public class UnifiedRuntimeDerivedProjectionService { private final UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService; private final UnifiedEventTimelineReconstructor timelineReconstructor; - private final DriverTimelineBuilder driverTimelineBuilder; - private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; private final DriverWorkingTimeProcessingCore workingTimeProcessingCore; private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer; private final EventHubProperties properties; @@ -56,18 +54,17 @@ public class UnifiedRuntimeDerivedProjectionService { public UnifiedRuntimeDerivedProjectionService( UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService, UnifiedEventTimelineReconstructor timelineReconstructor, - DriverTimelineBuilder driverTimelineBuilder, - DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventHubProperties properties, + DriverWorkingTimeReusableProjectionBuilder reusableProjectionBuilder, RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer ) { this( runtimeEventAssemblyService, timelineReconstructor, - driverTimelineBuilder, - reusableProjectionBuilder, properties, - new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine(reusableProjectionBuilder)), + new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine( + reusableProjectionBuilder + )), supportEvidenceNormalizer ); } @@ -76,16 +73,12 @@ public class UnifiedRuntimeDerivedProjectionService { public UnifiedRuntimeDerivedProjectionService( UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService, UnifiedEventTimelineReconstructor timelineReconstructor, - DriverTimelineBuilder driverTimelineBuilder, - DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventHubProperties properties, DriverWorkingTimeProcessingCore workingTimeProcessingCore, RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer ) { this.runtimeEventAssemblyService = runtimeEventAssemblyService; this.timelineReconstructor = timelineReconstructor; - this.driverTimelineBuilder = driverTimelineBuilder; - this.reusableProjectionBuilder = reusableProjectionBuilder; this.properties = properties; this.workingTimeProcessingCore = workingTimeProcessingCore; this.supportEvidenceNormalizer = supportEvidenceNormalizer; @@ -156,11 +149,15 @@ public class UnifiedRuntimeDerivedProjectionService { significantDrivingMinutes, minimumRestPeriodMinutes, timeline.activityIntervals().stream() - .map(interval -> DriverWorkingTimeActivityInterval.fromResolved(runtimeSessionId(request), driverKey, interval)) + .map(interval -> TachographDriverWorkingTimeAdapter.toActivityInterval( + runtimeSessionId(request), + driverKey, + interval + )) .filter(Objects::nonNull) .toList(), timeline.vehicleUsageIntervals().stream() - .map(DriverWorkingTimeVehicleUsageInterval::fromResolved) + .map(TachographDriverWorkingTimeAdapter::toVehicleUsageInterval) .filter(Objects::nonNull) .toList(), timeline.supportEvents().stream() 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 f788d6d..73b434c 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -1,9 +1,18 @@ package at.procon.eventhub.tachographfilesession.dto; import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeGeoEvidenceEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeSupportGeoEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; 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.TachographEsperGeoEvidenceEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; @@ -78,19 +87,19 @@ public record TachographEsperDriverProcessingResultDto( result.vehicleUsageIntervalCount(), result.vuCardAbsentIntervalCount(), result.supportGeoEventCount(), - result.activityIntervals(), - result.drivingIntervals(), - result.drivingInterruptionIntervals(), - result.drivingInterruptionVehicleChangeIntervals(), - result.dailyWeeklyRestCandidateIntervals(), - result.dailyWeeklyRestCandidateCoverageIntervals(), - result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(), - result.potentialHomeOvernightStayIntervals(), - result.potentialInVehicleOvernightStayIntervals(), - result.potentialInVehicleTripIntervals(), - result.vehicleUsageIntervals(), - result.vuCardAbsentIntervals(), - result.supportGeoEvents(), + mapActivityIntervalsToLegacy(result.activityIntervals()), + mapActivityIntervalsToLegacy(result.drivingIntervals()), + mapDrivingInterruptionsToLegacy(result.drivingInterruptionIntervals()), + mapDrivingInterruptionsToLegacy(result.drivingInterruptionVehicleChangeIntervals()), + mapDrivingInterruptionsToLegacy(result.dailyWeeklyRestCandidateIntervals()), + mapCoverageIntervalsToLegacy(result.dailyWeeklyRestCandidateCoverageIntervals()), + mapCoverageIntervalsToLegacy(result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals()), + mapPotentialHomeIntervalsToLegacy(result.potentialHomeOvernightStayIntervals()), + mapPotentialInVehicleIntervalsToLegacy(result.potentialInVehicleOvernightStayIntervals()), + mapPotentialTripsToLegacy(result.potentialInVehicleTripIntervals()), + mapVehicleUsageIntervalsToLegacy(result.vehicleUsageIntervals()), + mapVuCardAbsentIntervalsToLegacy(result.vuCardAbsentIntervals()), + mapSupportGeoEventsToLegacy(result.supportGeoEvents()), result.notes() ); } @@ -117,20 +126,689 @@ public record TachographEsperDriverProcessingResultDto( vehicleUsageIntervalCount, vuCardAbsentIntervalCount, supportGeoEventCount, - activityIntervals, - drivingIntervals, - drivingInterruptionIntervals, - drivingInterruptionVehicleChangeIntervals, - dailyWeeklyRestCandidateIntervals, - dailyWeeklyRestCandidateCoverageIntervals, - unclassifiedDailyWeeklyRestCandidateCoverageIntervals, - potentialHomeOvernightStayIntervals, - potentialInVehicleOvernightStayIntervals, - potentialInVehicleTripIntervals, - vehicleUsageIntervals, - vuCardAbsentIntervals, - supportGeoEvents, + mapActivityIntervalsFromLegacy(activityIntervals), + mapActivityIntervalsFromLegacy(drivingIntervals), + mapDrivingInterruptionsFromLegacy(drivingInterruptionIntervals), + mapDrivingInterruptionsFromLegacy(drivingInterruptionVehicleChangeIntervals), + mapDrivingInterruptionsFromLegacy(dailyWeeklyRestCandidateIntervals), + mapCoverageIntervalsFromLegacy(dailyWeeklyRestCandidateCoverageIntervals), + mapCoverageIntervalsFromLegacy(unclassifiedDailyWeeklyRestCandidateCoverageIntervals), + mapPotentialHomeIntervalsFromLegacy(potentialHomeOvernightStayIntervals), + mapPotentialInVehicleIntervalsFromLegacy(potentialInVehicleOvernightStayIntervals), + mapPotentialTripsFromLegacy(potentialInVehicleTripIntervals), + mapVehicleUsageIntervalsFromLegacy(vehicleUsageIntervals), + mapVuCardAbsentIntervalsFromLegacy(vuCardAbsentIntervals), + mapSupportGeoEventsFromLegacy(supportGeoEvents), notes ); } + + private static List mapActivityIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapActivityIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapDrivingInterruptionsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapDrivingInterruptionsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapCoverageIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacyCoverage).toList(); + } + + private static List mapCoverageIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapPotentialHomeIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacyPotentialHome).toList(); + } + + private static List mapPotentialHomeIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapPotentialInVehicleIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacyPotentialInVehicle).toList(); + } + + private static List mapPotentialInVehicleIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapPotentialTripsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapPotentialTripsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapVehicleUsageIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapVehicleUsageIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapVuCardAbsentIntervalsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapVuCardAbsentIntervalsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static List mapSupportGeoEventsToLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toLegacy).toList(); + } + + private static List mapSupportGeoEventsFromLegacy( + List values + ) { + return safe(values).stream().map(TachographEsperDriverProcessingResultDto::toCommon).toList(); + } + + private static DriverWorkingTimeActivityInterval toCommon(TachographEsperActivityIntervalEvent value) { + if (value == null) { + return null; + } + return new DriverWorkingTimeActivityInterval( + value.sessionId(), + value.driverKey(), + value.intervalId(), + value.activityType(), + value.cardSlot(), + value.cardStatus(), + value.drivingStatus(), + value.registrationKey(), + value.vehicleKey(), + value.sourceKind(), + firstSourceIntervalId(value.intervalId(), value.sourceIntervalIds()), + lastSourceIntervalId(value.intervalId(), value.sourceIntervalIds()), + value.startedAt(), + value.endedAt(), + epochSecond(value.startedAt()), + epochSecond(value.endedAt()), + value.durationSeconds(), + value.sourceIntervalIds(), + value.synthetic(), + value.clippedToRequestedPeriod(), + value.level() + ); + } + + private static TachographEsperActivityIntervalEvent toLegacy(DriverWorkingTimeActivityInterval value) { + return new TachographEsperActivityIntervalEvent( + value.sessionId(), + value.driverKey(), + value.intervalId(), + value.activityType(), + value.cardSlot(), + value.cardStatus(), + value.drivingStatus(), + value.registrationKey(), + value.vehicleKey(), + value.sourceKind(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.sourceIntervalIds(), + value.synthetic(), + value.clippedToRequestedPeriod(), + value.level() + ); + } + + private static DriverWorkingTimeDrivingInterruptionInterval toCommon( + TachographEsperDrivingInterruptionIntervalEvent value + ) { + if (value == null) { + return null; + } + return new DriverWorkingTimeDrivingInterruptionInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static TachographEsperDrivingInterruptionIntervalEvent toLegacy( + DriverWorkingTimeDrivingInterruptionInterval value + ) { + return new TachographEsperDrivingInterruptionIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static DriverWorkingTimeRestCoverageInterval toCommon( + TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent value + ) { + if (value == null) { + return null; + } + return new DriverWorkingTimeRestCoverageInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toCommon(value.beginGeoEvent()), + toCommon(value.endGeoEvent()) + ); + } + + private static DriverWorkingTimeRestCoverageInterval toCommon( + TachographEsperPotentialHomeOvernightStayIntervalEvent value + ) { + if (value == null) { + return null; + } + return new DriverWorkingTimeRestCoverageInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toCommon(value.beginGeoEvent()), + toCommon(value.endGeoEvent()) + ); + } + + private static DriverWorkingTimeRestCoverageInterval toCommon( + TachographEsperPotentialInVehicleOvernightStayIntervalEvent value + ) { + if (value == null) { + return null; + } + return new DriverWorkingTimeRestCoverageInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toCommon(value.beginGeoEvent()), + toCommon(value.endGeoEvent()) + ); + } + + private static TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent toLegacyCoverage( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static TachographEsperPotentialHomeOvernightStayIntervalEvent toLegacyPotentialHome( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperPotentialHomeOvernightStayIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static TachographEsperPotentialInVehicleOvernightStayIntervalEvent toLegacyPotentialInVehicle( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static DriverWorkingTimePotentialInVehicleTripInterval toCommon( + TachographEsperPotentialInVehicleTripIntervalEvent value + ) { + if (value == null) { + return null; + } + return new DriverWorkingTimePotentialInVehicleTripInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.registrationKey(), + value.vehicleKey(), + value.containedPotentialInVehicleOvernightStayIntervalCount(), + value.containedPotentialInVehicleOvernightStayDurationSeconds(), + value.containedCardAbsentDurationSeconds(), + value.firstPotentialInVehicleOvernightStayStartedAt(), + value.lastPotentialInVehicleOvernightStayEndedAt(), + value.firstPreviousDrivingSourceIntervalId(), + value.lastNextDrivingSourceIntervalId(), + safe(value.potentialInVehicleOvernightStayIntervals()).stream() + .map(TachographEsperDriverProcessingResultDto::toCommon) + .toList() + ); + } + + private static TachographEsperPotentialInVehicleTripIntervalEvent toLegacy( + DriverWorkingTimePotentialInVehicleTripInterval value + ) { + return new TachographEsperPotentialInVehicleTripIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.registrationKey(), + value.vehicleKey(), + value.containedPotentialInVehicleOvernightStayIntervalCount(), + value.containedPotentialInVehicleOvernightStayDurationSeconds(), + value.containedCardAbsentDurationSeconds(), + value.firstPotentialInVehicleOvernightStayStartedAt(), + value.lastPotentialInVehicleOvernightStayEndedAt(), + value.firstPreviousDrivingSourceIntervalId(), + value.lastNextDrivingSourceIntervalId(), + safe(value.potentialInVehicleOvernightStayIntervals()).stream() + .map(TachographEsperDriverProcessingResultDto::toLegacyPotentialInVehicle) + .toList() + ); + } + + private static DriverWorkingTimeVehicleUsageInterval toCommon(TachographEsperVehicleUsageIntervalEvent value) { + if (value == null) { + return null; + } + return new DriverWorkingTimeVehicleUsageInterval( + value.sessionId(), + value.driverKey(), + value.intervalId(), + firstSourceIntervalId(value.intervalId(), value.sourceIntervalIds()), + lastSourceIntervalId(value.intervalId(), value.sourceIntervalIds()), + value.startedAt(), + value.endedAt(), + epochSecond(value.startedAt()), + value.endedAt() == null ? null : value.endedAt().toEpochSecond(), + value.durationSeconds(), + value.odometerBeginKm(), + value.odometerEndKm(), + value.registrationKey(), + value.vehicleKey(), + value.sourceKind(), + value.sourceIntervalIds() + ); + } + + private static TachographEsperVehicleUsageIntervalEvent toLegacy(DriverWorkingTimeVehicleUsageInterval value) { + return new TachographEsperVehicleUsageIntervalEvent( + value.sessionId(), + value.driverKey(), + value.intervalId(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.odometerBeginKm(), + value.odometerEndKm(), + value.registrationKey(), + value.vehicleKey(), + value.sourceKind(), + value.sourceIntervalIds() + ); + } + + private static DriverWorkingTimeVuCardAbsentInterval toCommon(TachographEsperVuCardAbsentIntervalEvent value) { + if (value == null) { + return null; + } + return new DriverWorkingTimeVuCardAbsentInterval( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousUsageIntervalId(), + value.nextUsageIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static TachographEsperVuCardAbsentIntervalEvent toLegacy(DriverWorkingTimeVuCardAbsentInterval value) { + return new TachographEsperVuCardAbsentIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousUsageIntervalId(), + value.nextUsageIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static DriverWorkingTimeSupportGeoEvent toCommon(TachographEsperSupportGeoEvent value) { + if (value == null) { + return null; + } + return new DriverWorkingTimeSupportGeoEvent( + value.eventId(), + value.driverKey(), + value.occurredAt(), + value.eventDomain(), + value.eventType(), + value.eventLifecycle(), + value.registrationKey(), + value.vehicleKey(), + value.country(), + value.region(), + value.countryFrom(), + value.countryTo(), + value.operation(), + value.latitude(), + value.longitude(), + value.odometerKm(), + value.rawRecordPath() + ); + } + + private static TachographEsperSupportGeoEvent toLegacy(DriverWorkingTimeSupportGeoEvent value) { + return new TachographEsperSupportGeoEvent( + value.eventId(), + value.driverKey(), + value.occurredAt(), + value.eventDomain(), + value.eventType(), + value.eventLifecycle(), + value.registrationKey(), + value.vehicleKey(), + value.country(), + value.region(), + value.countryFrom(), + value.countryTo(), + value.operation(), + value.latitude(), + value.longitude(), + value.odometerKm(), + value.rawRecordPath() + ); + } + + private static DriverWorkingTimeGeoEvidenceEvent toCommon(TachographEsperGeoEvidenceEvent value) { + if (value == null) { + return null; + } + return new DriverWorkingTimeGeoEvidenceEvent( + value.eventId(), + value.eventDomain(), + value.occurredAt(), + value.latitude(), + value.longitude(), + value.distanceSeconds(), + value.odometerKm() + ); + } + + private static TachographEsperGeoEvidenceEvent toLegacy(DriverWorkingTimeGeoEvidenceEvent value) { + if (value == null) { + return null; + } + return new TachographEsperGeoEvidenceEvent( + value.eventId(), + value.eventDomain(), + value.occurredAt(), + value.latitude(), + value.longitude(), + value.distanceSeconds(), + value.odometerKm() + ); + } + + private static String firstSourceIntervalId(String intervalId, List sourceIntervalIds) { + return sourceIntervalIds == null || sourceIntervalIds.isEmpty() ? intervalId : sourceIntervalIds.getFirst(); + } + + private static String lastSourceIntervalId(String intervalId, List sourceIntervalIds) { + return sourceIntervalIds == null || sourceIntervalIds.isEmpty() + ? intervalId + : sourceIntervalIds.getLast(); + } + + private static long epochSecond(OffsetDateTime value) { + return value == null ? 0L : value.toEpochSecond(); + } + + private static List safe(List values) { + return values == null ? List.of() : values; + } } 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 7852ef2..0b99bf5 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -1,210 +1,113 @@ 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 com.fasterxml.jackson.databind.JsonNode; import at.procon.eventhub.config.EventHubProperties; -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.processing.service.UnifiedEventTimelineReconstructor; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeReusableProjectionBuilder; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; -import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; -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.TachographEsperGeoEvidenceEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; -import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; -import java.io.IOException; -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.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.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; -import org.springframework.util.StreamUtils; +/** + * @deprecated Legacy forwarding shell kept only for external callers that still reference the old + * type. Prefer {@link TachographDerivedProjectionCompatibilityFacade} for tachograph-owned usage + * or {@link DriverWorkingTimeReusableProjectionBuilder} / the neutral processing core for shared + * processing. + */ @Component +@Deprecated(forRemoval = false) public class DriverTimelineReusableProjectionBuilder { - private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); - private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE = - loadResource("esper/driver-working-time-derived-projections.epl"); - private static final String DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL = - loadResource("esper/runtime-driver-event-interval-preprocessor.epl"); - - private final DriverTimelineBuilder driverTimelineBuilder; - private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder; - private final UnifiedEventTimelineReconstructor timelineReconstructor; - private final EventHubProperties properties; + private final TachographDerivedProjectionCompatibilityFacade delegate; + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public DriverTimelineReusableProjectionBuilder( DriverTimelineBuilder driverTimelineBuilder, EventHubProperties properties ) { - this(driverTimelineBuilder, null, new UnifiedEventTimelineReconstructor(), properties); + this(new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, properties)); } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public DriverTimelineReusableProjectionBuilder( DriverTimelineBuilder driverTimelineBuilder, RawSourceDriverTimelineEventBuilder rawSourceEventBuilder, EventHubProperties properties ) { - this(driverTimelineBuilder, rawSourceEventBuilder, new UnifiedEventTimelineReconstructor(), properties); + this(new TachographDerivedProjectionCompatibilityFacade( + driverTimelineBuilder, + rawSourceEventBuilder, + properties + )); } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ @Autowired + @Deprecated(forRemoval = false) public DriverTimelineReusableProjectionBuilder( - DriverTimelineBuilder driverTimelineBuilder, - RawSourceDriverTimelineEventBuilder rawSourceEventBuilder, - UnifiedEventTimelineReconstructor timelineReconstructor, - EventHubProperties properties + TachographDerivedProjectionCompatibilityFacade delegate ) { - this.driverTimelineBuilder = driverTimelineBuilder; - this.rawSourceEventBuilder = rawSourceEventBuilder; - this.timelineReconstructor = timelineReconstructor == null - ? new UnifiedEventTimelineReconstructor() - : timelineReconstructor; - this.properties = properties; + this.delegate = delegate; } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade#commonBuilder()}. + */ + @Deprecated(forRemoval = false) + public DriverWorkingTimeReusableProjectionBuilder commonBuilder() { + return delegate.commonBuilder(); + } + + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( TachographFileSession session, DriverExtractionSession driverSession, int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - if (session == null || driverSession == null) { - return emptyBundle(); - } - if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) { - return buildEsperDrivingDerivedProjectionBundleFromEvents( - session.sessionId(), - driverSession.driverKey(), - rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents(), - significantDrivingMinutes, - minimumRestPeriodMinutes - ); - } - ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); - return buildEsperDrivingDerivedProjectionBundle( - session.sessionId(), - driverSession.driverKey(), - timeline, + return delegate.buildEsperDrivingDerivedProjectionBundle( + session, + driverSession, significantDrivingMinutes, minimumRestPeriodMinutes ); } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( TachographFileSession session, int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - if (session == null || session.driversByKey() == null || session.driversByKey().isEmpty()) { - return emptyBundle(); - } - - if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) { - List> activityInputEvents = new ArrayList<>(); - List> vehicleUsageInputEvents = new ArrayList<>(); - List> supportGeoInputEvents = new ArrayList<>(); - - for (DriverExtractionSession driverSession : session.driversByKey().values()) { - if (driverSession == null || driverSession.driverKey() == null) { - continue; - } - ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents( - session.sessionId(), - driverSession.driverKey(), - rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents() - ); - if (timeline == null) { - continue; - } - activityInputEvents.addAll(buildActivityIntervalInputEvents( - session.sessionId(), - driverSession.driverKey(), - timeline.activityIntervals() - )); - vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals())); - supportGeoInputEvents.addAll(buildSupportGeoInputEvents(session.sessionId(), timeline.supportEvents())); - } - - return buildEsperDrivingDerivedProjectionBundle( - activityInputEvents, - vehicleUsageInputEvents, - supportGeoInputEvents, - significantDrivingMinutes, - minimumRestPeriodMinutes - ); - } - - List> activityInputEvents = new ArrayList<>(); - List> vehicleUsageInputEvents = new ArrayList<>(); - List> supportGeoInputEvents = new ArrayList<>(); - - for (DriverExtractionSession driverSession : session.driversByKey().values()) { - if (driverSession == null || driverSession.driverKey() == null) { - continue; - } - ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); - if (timeline == null) { - continue; - } - for (ResolvedActivityInterval interval : safeList(timeline.activityIntervals())) { - activityInputEvents.add(toActivityIntervalInputMap( - session.sessionId(), - driverSession.driverKey(), - interval - )); - } - for (ResolvedVehicleUsageInterval interval : safeList(timeline.vehicleUsageIntervals())) { - vehicleUsageInputEvents.add(toVehicleUsageIntervalInputMap(interval)); - } - for (ExtractedSupportEvent supportEvent : safeList(timeline.supportEvents())) { - Map supportGeoEvidence = toSupportGeoEvidenceInputMap(session.sessionId(), supportEvent); - if (supportGeoEvidence != null) { - supportGeoInputEvents.add(supportGeoEvidence); - } - } - } - - return buildEsperDrivingDerivedProjectionBundle( - activityInputEvents, - vehicleUsageInputEvents, - supportGeoInputEvents, + return delegate.buildEsperDrivingDerivedProjectionBundle( + session, significantDrivingMinutes, minimumRestPeriodMinutes ); } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( UUID sessionId, String driverKey, @@ -212,122 +115,35 @@ public class DriverTimelineReusableProjectionBuilder { int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - if (timeline == null) { - return emptyBundle(); - } - return buildEsperDrivingDerivedProjectionBundle( - buildActivityIntervalInputEvents(sessionId, driverKey, timeline.activityIntervals()), - buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()), - buildSupportGeoInputEvents(sessionId, timeline.supportEvents()), + return delegate.buildEsperDrivingDerivedProjectionBundle( + sessionId, + driverKey, + timeline, significantDrivingMinutes, minimumRestPeriodMinutes ); } - private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( - List> activityInputEvents, - List> vehicleUsageInputEvents, - List> supportGeoInputEvents, - int significantDrivingMinutes, - int minimumRestPeriodMinutes - ) { - if ((activityInputEvents == null || activityInputEvents.isEmpty()) - && (vehicleUsageInputEvents == null || vehicleUsageInputEvents.isEmpty())) { - return emptyBundle(); - } - - 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<>(); - List potentialInVehicleTripIntervals = new ArrayList<>(); - - executeWithRuntime( - configuration -> { - configuration.getCommon().addEventType( - "TachographActivityIntervalInputEvent", - activityIntervalInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographVehicleUsageIntervalInputEvent", - vehicleUsageIntervalInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographSupportGeoEvidenceInputEvent", - supportGeoEvidenceInputDefinition() - ); - }, - renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes), - 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), - "potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals), - "potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals) - ), - runtime -> { - if (supportGeoInputEvents != null) { - for (Map supportGeoEvidence : supportGeoInputEvents) { - runtime.getEventService().sendEventMap( - supportGeoEvidence, - "TachographSupportGeoEvidenceInputEvent" - ); - } - } - if (vehicleUsageInputEvents != null) { - for (Map interval : vehicleUsageInputEvents) { - runtime.getEventService().sendEventMap( - interval, - "TachographVehicleUsageIntervalInputEvent" - ); - } - } - if (activityInputEvents != null) { - for (Map interval : activityInputEvents) { - runtime.getEventService().sendEventMap( - interval, - "TachographActivityIntervalInputEvent" - ); - } - } - } - ); - - return new TachographEsperDrivingDerivedProjectionBundle( - sortDrivingInterruptionIntervals(drivingInterruptionIntervals), - sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals), - sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals), - sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals), - sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), - sortVuCardAbsentIntervals(vuCardAbsentIntervals), - sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals), - sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals), - sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals) - ); - } - + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents( List events, int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - return buildEsperDrivingDerivedProjectionBundleFromEvents( - null, - null, + return delegate.buildEsperDrivingDerivedProjectionBundleFromEvents( events, significantDrivingMinutes, minimumRestPeriodMinutes ); } + /** + * @deprecated Use {@link TachographDerivedProjectionCompatibilityFacade}. + */ + @Deprecated(forRemoval = false) public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents( UUID fallbackSessionId, String fallbackDriverKey, @@ -335,1426 +151,12 @@ public class DriverTimelineReusableProjectionBuilder { int significantDrivingMinutes, int minimumRestPeriodMinutes ) { - if (fallbackDriverKey == null) { - Map> eventsByDriver = groupEventsByDriverKey(events); - if (eventsByDriver.size() > 1) { - return buildEsperDrivingDerivedProjectionBundleFromGroupedEvents( - fallbackSessionId, - eventsByDriver, - significantDrivingMinutes, - minimumRestPeriodMinutes - ); - } - if (eventsByDriver.size() == 1) { - Map.Entry> onlyDriver = eventsByDriver.entrySet().iterator().next(); - fallbackDriverKey = onlyDriver.getKey(); - events = onlyDriver.getValue(); - } - } - - ResolvedDriverTimeline reconstructedTimeline = reconstructMergedTimelineFromEvents( + return delegate.buildEsperDrivingDerivedProjectionBundleFromEvents( fallbackSessionId, fallbackDriverKey, - events - ); - return buildEsperDrivingDerivedProjectionBundle( - fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId, - fallbackDriverKey, - reconstructedTimeline, + events, significantDrivingMinutes, minimumRestPeriodMinutes ); } - - private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromGroupedEvents( - UUID fallbackSessionId, - Map> eventsByDriver, - int significantDrivingMinutes, - int minimumRestPeriodMinutes - ) { - List> activityInputEvents = new ArrayList<>(); - List> vehicleUsageInputEvents = new ArrayList<>(); - List> supportGeoInputEvents = new ArrayList<>(); - UUID sessionId = fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId; - - for (Map.Entry> entry : eventsByDriver.entrySet()) { - String driverKey = entry.getKey(); - if (driverKey == null || driverKey.isBlank()) { - continue; - } - ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents( - sessionId, - driverKey, - entry.getValue() - ); - if (timeline == null) { - continue; - } - activityInputEvents.addAll(buildActivityIntervalInputEvents( - sessionId, - driverKey, - timeline.activityIntervals() - )); - vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals())); - supportGeoInputEvents.addAll(buildSupportGeoInputEvents(sessionId, timeline.supportEvents())); - } - - return buildEsperDrivingDerivedProjectionBundle( - activityInputEvents, - vehicleUsageInputEvents, - supportGeoInputEvents, - significantDrivingMinutes, - minimumRestPeriodMinutes - ); - } - - private Map> groupEventsByDriverKey(List events) { - Map> grouped = new LinkedHashMap<>(); - for (EventHubEventDto event : safeList(events)) { - String driverKey = eventDriverKey(event); - if (driverKey == null || driverKey.isBlank()) { - continue; - } - grouped.computeIfAbsent(driverKey, ignored -> new ArrayList<>()).add(event); - } - return grouped; - } - - private String eventDriverKey(EventHubEventDto event) { - if (event == null) { - return null; - } - JsonNode raw = rawPayload(event); - return firstNonBlank(text(raw, "driverKey"), driverKey(event)); - } - - private ResolvedDriverTimeline reconstructMergedTimelineFromEvents( - UUID fallbackSessionId, - String fallbackDriverKey, - List events - ) { - ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct( - fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId, - fallbackDriverKey, - safeList(events) - ); - return new ResolvedDriverTimeline( - reconstructed.sourceKind(), - reconstructed.loadedFrom(), - reconstructed.loadedTo(), - mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()), - reconstructed.activityIntervals(), - reconstructed.supportEvents(), - reconstructed.warnings() - ); - } - - private List mergeVehicleUsageIntervals( - List intervals, - String sourceKind - ) { - List sorted = safeList(intervals).stream() - .sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from) - .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))) - .toList(); - if (sorted.isEmpty()) { - return List.of(); - } - - List result = new ArrayList<>(); - ResolvedVehicleUsageInterval current = sorted.getFirst(); - List currentSources = new ArrayList<>(current.sourceIntervalIds()); - for (int i = 1; i < sorted.size(); i++) { - ResolvedVehicleUsageInterval next = sorted.get(i); - if (canMergeVehicleUsage(current, next)) { - currentSources.addAll(next.sourceIntervalIds()); - current = ResolvedVehicleUsageInterval.resolved( - current.sessionId(), - current.driverKey(), - current.intervalId() + "+" + next.intervalId(), - current.from(), - mergedTo(current.to(), next.to()), - current.odometerBeginKm(), - mergedOdometerEnd(current, next), - current.registrationKey(), - current.vehicleKey(), - sourceKind, - currentSources - ); - } else { - result.add(current); - current = next; - currentSources = new ArrayList<>(current.sourceIntervalIds()); - } - } - result.add(current); - return result; - } - - private boolean canMergeVehicleUsage(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) { - return Objects.equals(left.registrationKey(), right.registrationKey()) - && Objects.equals(left.vehicleKey(), right.vehicleKey()) - && !right.from().isAfter(mergeBoundary(left.to())); - } - - private Long mergedOdometerEnd( - ResolvedVehicleUsageInterval current, - ResolvedVehicleUsageInterval next - ) { - if (current == null) { - return next == null ? null : next.odometerEndKm(); - } - if (next == null) { - return current.odometerEndKm(); - } - if (current.to() == null || next.to() == null) { - return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm(); - } - if (next.to().isAfter(current.to()) || next.to().isEqual(current.to())) { - return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm(); - } - return current.odometerEndKm() != null ? current.odometerEndKm() : next.odometerEndKm(); - } - - private OffsetDateTime mergedTo(OffsetDateTime left, OffsetDateTime right) { - if (left == null || right == null) { - return null; - } - return left.isAfter(right) ? left : right; - } - - private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) { - return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1); - } - - private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromPointInput( - List> activityPointInputEvents, - List> vehicleUsagePointInputEvents, - List> supportGeoInputEvents, - int significantDrivingMinutes, - int minimumRestPeriodMinutes - ) { - if ((activityPointInputEvents == null || activityPointInputEvents.isEmpty()) - && (vehicleUsagePointInputEvents == null || vehicleUsagePointInputEvents.isEmpty())) { - return emptyBundle(); - } - - 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<>(); - List potentialInVehicleTripIntervals = new ArrayList<>(); - - executeWithRuntime( - configuration -> { - configuration.getCommon().addEventType( - "TachographActivityPointInputEvent", - activityPointInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographVehicleUsagePointInputEvent", - vehicleUsagePointInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographProjectionFinalizeEvent", - projectionFinalizeInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographActivityIntervalInputEvent", - activityIntervalInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographVehicleUsageIntervalInputEvent", - vehicleUsageIntervalInputDefinition() - ); - configuration.getCommon().addEventType( - "TachographSupportGeoEvidenceInputEvent", - supportGeoEvidenceInputDefinition() - ); - }, - renderDrivingDerivedProjectionEventsEpl(significantDrivingMinutes, minimumRestPeriodMinutes), - 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), - "potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals), - "potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals) - ), - runtime -> { - if (supportGeoInputEvents != null) { - for (Map supportGeoEvidence : supportGeoInputEvents) { - runtime.getEventService().sendEventMap( - supportGeoEvidence, - "TachographSupportGeoEvidenceInputEvent" - ); - } - } - if (vehicleUsagePointInputEvents != null) { - for (Map point : vehicleUsagePointInputEvents) { - runtime.getEventService().sendEventMap( - point, - "TachographVehicleUsagePointInputEvent" - ); - } - for (Map finalizeEvent : buildProjectionFinalizeEvents(vehicleUsagePointInputEvents)) { - runtime.getEventService().sendEventMap( - finalizeEvent, - "TachographProjectionFinalizeEvent" - ); - } - } - if (activityPointInputEvents != null) { - for (Map point : activityPointInputEvents) { - runtime.getEventService().sendEventMap( - point, - "TachographActivityPointInputEvent" - ); - } - } - } - ); - - return new TachographEsperDrivingDerivedProjectionBundle( - sortDrivingInterruptionIntervals(drivingInterruptionIntervals), - sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals), - sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals), - sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals), - sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), - sortVuCardAbsentIntervals(vuCardAbsentIntervals), - sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals), - sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals), - sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals) - ); - } - - private List> buildProjectionFinalizeEvents( - List> vehicleUsagePointInputEvents - ) { - Map> byDriver = new LinkedHashMap<>(); - for (Map point : safeList(vehicleUsagePointInputEvents)) { - String driverKey = Objects.toString(point.get("driverKey"), null); - if (driverKey == null || driverKey.isBlank()) { - continue; - } - Map finalizeEvent = byDriver.computeIfAbsent(driverKey, ignored -> { - Map event = new LinkedHashMap<>(); - event.put("sessionId", point.get("sessionId")); - event.put("driverKey", driverKey); - event.put("finalizedAtEpochSecond", 0L); - return event; - }); - Long occurredAtEpochSecond = (Long) point.get("occurredAtEpochSecond"); - Long finalizedAtEpochSecond = (Long) finalizeEvent.get("finalizedAtEpochSecond"); - if (occurredAtEpochSecond != null - && (finalizedAtEpochSecond == null || occurredAtEpochSecond > finalizedAtEpochSecond)) { - finalizeEvent.put("finalizedAtEpochSecond", occurredAtEpochSecond); - } - } - return new ArrayList<>(byDriver.values()); - } - - private List> buildActivityIntervalInputEvents( - UUID sessionId, - String driverKey, - List activityIntervals - ) { - return safeList(activityIntervals).stream() - .map(interval -> toActivityIntervalInputMap(sessionId, driverKey, interval)) - .sorted(Comparator - .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) - .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) - .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) - .toList(); - } - - private List> buildVehicleUsageIntervalInputEvents( - List vehicleUsageIntervals - ) { - return safeList(vehicleUsageIntervals).stream() - .map(this::toVehicleUsageIntervalInputMap) - .sorted(Comparator - .comparing((Map event) -> (Long) event.get("startedAtEpochSecond")) - .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) - .thenComparing(event -> Objects.toString(event.get("intervalId"), ""))) - .toList(); - } - - private List> buildSupportGeoInputEvents( - UUID sessionId, - List supportEvents - ) { - return safeList(supportEvents).stream() - .map(event -> toSupportGeoEvidenceInputMap(sessionId, event)) - .filter(Objects::nonNull) - .sorted(Comparator - .comparing((Map event) -> (Long) event.get("occurredAtEpochSecond")) - .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) - .thenComparing(event -> Objects.toString(event.get("eventId"), ""))) - .toList(); - } - - private List safeList(List values) { - return values == null ? List.of() : values; - } - - private TachographEsperDrivingDerivedProjectionBundle emptyBundle() { - return new TachographEsperDrivingDerivedProjectionBundle( - List.of(), - List.of(), - List.of(), - List.of(), - List.of(), - List.of(), - List.of(), - List.of(), - List.of() - ); - } - - private void executeWithRuntime( - Consumer configurationSetup, - String epl, - Map> listeners, - Consumer sender - ) { - EPRuntime runtime = null; - try { - Configuration configuration = new Configuration(); - configurationSetup.accept(configuration); - String runtimeUri = "eventhub-tachograph-reusable-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); - for (Map.Entry> entry : listeners.entrySet()) { - runtime.getDeploymentService() - .getStatement(deployment.getDeploymentId(), entry.getKey()) - .addListener((newData, oldData, statement, rt) -> entry.getValue().accept(newData)); - } - - sender.accept(runtime); - } catch (EPCompileException | EPDeployException e) { - throw new IllegalStateException("Cannot compile/deploy reusable tachograph projection EPL bundle", e); - } finally { - if (runtime != null) { - runtime.destroy(); - } - } - } - - private Map activityPointInputDefinition() { - Map definition = new LinkedHashMap<>(); - definition.put("sessionId", UUID.class); - definition.put("driverKey", String.class); - definition.put("eventId", String.class); - definition.put("intervalId", String.class); - definition.put("sourceRowId", String.class); - definition.put("sourceRowIds", java.util.List.class); - definition.put("activityType", String.class); - definition.put("lifecycle", String.class); - definition.put("occurredAt", OffsetDateTime.class); - definition.put("occurredAtEpochSecond", long.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("synthetic", boolean.class); - definition.put("clippedToRequestedPeriod", boolean.class); - definition.put("level", String.class); - return definition; - } - - private Map vehicleUsagePointInputDefinition() { - Map definition = new LinkedHashMap<>(); - definition.put("sessionId", UUID.class); - definition.put("driverKey", String.class); - definition.put("eventId", String.class); - definition.put("intervalId", String.class); - definition.put("sourceRowId", String.class); - definition.put("sourceRowIds", java.util.List.class); - definition.put("lifecycle", String.class); - definition.put("occurredAt", OffsetDateTime.class); - definition.put("occurredAtEpochSecond", long.class); - definition.put("registrationKey", String.class); - definition.put("vehicleKey", String.class); - definition.put("sourceKind", String.class); - definition.put("odometerKm", Long.class); - return definition; - } - - private Map projectionFinalizeInputDefinition() { - Map definition = new LinkedHashMap<>(); - definition.put("sessionId", UUID.class); - definition.put("driverKey", String.class); - definition.put("finalizedAtEpochSecond", long.class); - return definition; - } - - private Map activityIntervalInputDefinition() { - Map 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 vehicleUsageIntervalInputDefinition() { - Map 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 supportGeoEvidenceInputDefinition() { - Map definition = new LinkedHashMap<>(); - definition.put("sessionId", UUID.class); - definition.put("driverKey", String.class); - definition.put("eventId", String.class); - definition.put("eventDomain", String.class); - definition.put("occurredAt", OffsetDateTime.class); - definition.put("occurredAtEpochSecond", long.class); - definition.put("registrationKey", String.class); - definition.put("vehicleKey", String.class); - definition.put("latitude", Double.class); - definition.put("longitude", Double.class); - definition.put("odometerKm", Long.class); - definition.put("priority", int.class); - return definition; - } - - private Map toActivityIntervalInputMap( - UUID sessionId, - String driverKey, - ResolvedActivityInterval interval - ) { - Map 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 Map toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) { - Map 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 toSupportGeoEvidenceInputMap( - UUID sessionId, - ExtractedSupportEvent supportEvent - ) { - if (supportEvent == null - || supportEvent.driverKey() == null - || supportEvent.occurredAt() == null - || supportEvent.latitude() == null - || supportEvent.longitude() == null) { - return null; - } - int priority = supportGeoPriority(supportEvent.eventDomain()); - if (priority <= 0) { - return null; - } - Map event = new LinkedHashMap<>(); - event.put("sessionId", sessionId); - event.put("driverKey", supportEvent.driverKey()); - event.put("eventId", supportEvent.eventId()); - event.put("eventDomain", supportEvent.eventDomain()); - event.put("occurredAt", supportEvent.occurredAt()); - event.put("occurredAtEpochSecond", supportEvent.occurredAt().toEpochSecond()); - event.put("registrationKey", supportEvent.registrationKey()); - event.put("vehicleKey", supportEvent.vehicleKey()); - event.put("latitude", supportEvent.latitude().doubleValue()); - event.put("longitude", supportEvent.longitude().doubleValue()); - event.put("odometerKm", supportEvent.odometerKm()); - event.put("priority", priority); - return event; - } - - private Map toActivityPointInputMap( - UUID fallbackSessionId, - String fallbackDriverKey, - EventHubEventDto sourceEvent - ) { - if (sourceEvent == null - || sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY - || (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END) - || sourceEvent.occurredAt() == null) { - return null; - } - JsonNode raw = rawPayload(sourceEvent); - String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()); - String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent)); - if (driverKey == null || intervalId == null) { - return null; - } - JsonNode attributes = attributes(sourceEvent); - Map event = new LinkedHashMap<>(); - event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent)); - event.put("driverKey", driverKey); - event.put("eventId", sourceEvent.externalSourceEventId()); - event.put("intervalId", intervalId); - event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId)); - event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId)); - event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType()))); - event.put("lifecycle", sourceEvent.lifecycle().name()); - event.put("occurredAt", sourceEvent.occurredAt()); - event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond()); - event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot"))); - event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus"))); - event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus"))); - event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent))); - event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent))); - event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent))); - event.put("synthetic", booleanValue(raw, "synthetic", false)); - event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false)); - event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT")); - return event; - } - - private Map toVehicleUsagePointInputMap( - UUID fallbackSessionId, - String fallbackDriverKey, - EventHubEventDto sourceEvent - ) { - if (sourceEvent == null - || sourceEvent.eventDomain() != EventDomain.DRIVER_CARD - || (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW) - || sourceEvent.occurredAt() == null) { - return null; - } - boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED - || sourceEvent.eventType() == EventType.CARD_WITHDRAWN; - if (!supportedType) { - return null; - } - JsonNode raw = rawPayload(sourceEvent); - String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()); - String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent)); - if (driverKey == null || intervalId == null) { - return null; - } - Map event = new LinkedHashMap<>(); - event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent)); - event.put("driverKey", driverKey); - event.put("eventId", sourceEvent.externalSourceEventId()); - event.put("intervalId", intervalId); - event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId)); - event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId)); - event.put("lifecycle", sourceEvent.lifecycle().name()); - event.put("occurredAt", sourceEvent.occurredAt()); - event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond()); - event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent))); - event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent))); - event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent))); - event.put("odometerKm", odometerKm(sourceEvent, raw)); - return event; - } - - private Map toSupportGeoEvidenceInputMap( - UUID fallbackSessionId, - String fallbackDriverKey, - EventHubEventDto sourceEvent - ) { - if (sourceEvent == null || sourceEvent.occurredAt() == null || sourceEvent.position() == null) { - return null; - } - String eventDomain = sourceEvent.eventDomain() == null ? null : sourceEvent.eventDomain().name(); - int priority = supportGeoPriority(eventDomain); - if (priority <= 0) { - return null; - } - JsonNode raw = rawPayload(sourceEvent); - String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent)); - if (driverKey == null) { - return null; - } - Map event = new LinkedHashMap<>(); - event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent)); - event.put("driverKey", driverKey); - event.put("eventId", firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId())); - event.put("eventDomain", eventDomain); - event.put("occurredAt", sourceEvent.occurredAt()); - event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond()); - event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent))); - event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent))); - event.put("latitude", sourceEvent.position().latitude().doubleValue()); - event.put("longitude", sourceEvent.position().longitude().doubleValue()); - event.put("odometerKm", odometerKm(sourceEvent, raw)); - event.put("priority", priority); - return event; - } - - private int supportGeoPriority(String eventDomain) { - if (eventDomain == null || eventDomain.isBlank()) { - return 0; - } - return switch (eventDomain.trim().toUpperCase()) { - case "POSITION" -> 500; - case "PLACE" -> 400; - case "BORDER_CROSSING" -> 300; - case "LOAD_UNLOAD" -> 250; - default -> 0; - }; - } - - private Comparator> pointEventComparator() { - return Comparator - .comparing((Map event) -> (Long) event.get("occurredAtEpochSecond")) - .thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), ""))) - .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) - .thenComparing(event -> Objects.toString(event.get("intervalId"), "")) - .thenComparing(event -> Objects.toString(event.get("eventId"), "")); - } - - private int lifecycleOrder(String lifecycle) { - return switch (lifecycle) { - case "INSERT", "START" -> 0; - case "WITHDRAW", "END" -> 1; - default -> 2; - }; - } - - private EventHubProperties.DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode() { - return properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode(); - } - - private RawSourceDriverTimelineEventBuilder rawSourceEventBuilder() { - if (rawSourceEventBuilder == null) { - throw new IllegalStateException( - "Driving-derived projection input mode EVENTS requires RawSourceDriverTimelineEventBuilder" - ); - } - return rawSourceEventBuilder; - } - - private JsonNode rawPayload(EventHubEventDto event) { - JsonNode payload = event.payload(); - if (payload == null || payload.isNull()) { - return null; - } - JsonNode raw = payload.get("raw"); - return raw == null || raw.isNull() ? payload : raw; - } - - private JsonNode attributes(EventHubEventDto event) { - return event.eventDetails() == null ? null : event.eventDetails().attributes(); - } - - private String text(JsonNode node, String field) { - if (node == null || field == null) { - return null; - } - JsonNode value = node.get(field); - if (value == null || value.isNull()) { - return null; - } - String text = value.asText(null); - return text == null || text.isBlank() ? null : text; - } - - private boolean booleanValue(JsonNode node, String field, boolean fallback) { - if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) { - return fallback; - } - return node.get(field).asBoolean(fallback); - } - - private Long longValue(JsonNode node, String field) { - if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) { - return null; - } - JsonNode value = node.get(field); - if (value.isNumber()) { - return value.asLong(); - } - try { - return Long.parseLong(value.asText()); - } catch (NumberFormatException ignored) { - return null; - } - } - - private List stringList(JsonNode node, String field, String fallback) { - JsonNode value = node == null || field == null ? null : node.get(field); - if (value == null || value.isNull()) { - return fallback == null ? List.of() : List.of(fallback); - } - if (value.isArray()) { - List result = new ArrayList<>(); - value.forEach(item -> { - if (item != null && !item.isNull()) { - String text = item.asText(null); - if (text != null && !text.isBlank()) { - result.add(text); - } - } - }); - return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result); - } - String text = value.asText(null); - return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text); - } - - private String firstNonBlank(String... values) { - if (values == null) { - return null; - } - for (String value : values) { - if (value != null && !value.isBlank()) { - return value.trim(); - } - } - return null; - } - - private String eventTypeAsActivity(EventType eventType) { - if (eventType == null) { - return "UNKNOWN"; - } - return switch (eventType) { - case DRIVE -> "DRIVE"; - case WORK -> "WORK"; - case AVAILABILITY -> "AVAILABILITY"; - case BREAK_REST -> "BREAK_REST"; - default -> eventType.name(); - }; - } - - private UUID sessionId(UUID fallbackSessionId, JsonNode raw, EventHubEventDto event) { - String rawSessionId = firstNonBlank( - text(raw, "sessionId"), - event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId() - ); - if (rawSessionId != null) { - try { - return UUID.fromString(rawSessionId); - } catch (IllegalArgumentException ignored) { - // DB-acquired source packages need not be UUID-based file sessions. - } - } - return fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId; - } - - private String driverKey(EventHubEventDto event) { - if (event.driverRef() == null) { - return null; - } - if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) { - return event.driverRef().driverCard().stableKey(); - } - return event.driverRef().sourceEntityId(); - } - - private String registrationKey(EventHubEventDto event) { - if (event.vehicleRef() == null || event.vehicleRef().vehicleRegistration() == null) { - return null; - } - return event.vehicleRef().vehicleRegistration().stableKey(); - } - - private String vehicleKey(EventHubEventDto event) { - if (event.vehicleRef() == null) { - return null; - } - return firstNonBlank(event.vehicleRef().vin(), event.vehicleRef().sourceVehicleEntityId()); - } - - private String sourceKind(EventHubEventDto event) { - return event.packageInfo() == null || event.packageInfo().eventSource() == null - ? null - : event.packageInfo().eventSource().sourceKind(); - } - - private Long odometerKm(EventHubEventDto event, JsonNode raw) { - Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm"); - if (explicit != null) { - return explicit; - } - explicit = longValue(raw, "odometerKm"); - if (explicit != null) { - return explicit; - } - return event.odometerM() == null ? null : event.odometerM() / 1_000L; - } - - 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 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 collectDrivingInterruptionIntervalEvents( - 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 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 collectVuCardAbsentIntervalEvents( - 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 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 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"); - TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent( - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")) - ); - TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent( - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")) - ); - 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("cardAbsentDurationSeconds"), - (Double) event.get("cardAbsentCoveragePercent"), - (String) event.get("previousDrivingSourceIntervalId"), - (String) event.get("nextDrivingSourceIntervalId"), - (String) event.get("previousRegistrationKey"), - (String) event.get("nextRegistrationKey"), - (String) event.get("previousVehicleKey"), - (String) event.get("nextVehicleKey"), - longOrNull(event.get("beginBoundaryOdometerKm")), - longOrNull(event.get("endBoundaryOdometerKm")), - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")), - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")), - longOrNull(event.get("geoEvidenceMovementMeters")), - (String) event.get("geoEvidenceMovementCategory"), - beginGeoEvent, - endGeoEvent - )); - } - } - - private void collectPotentialHomeOvernightStayIntervalEvents( - EventBean[] newData, - List target - ) { - if (newData == null) { - return; - } - for (EventBean event : newData) { - long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); - long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); - TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent( - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")) - ); - TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent( - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")) - ); - target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent( - (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("cardAbsentDurationSeconds"), - (Double) event.get("cardAbsentCoveragePercent"), - (String) event.get("previousDrivingSourceIntervalId"), - (String) event.get("nextDrivingSourceIntervalId"), - (String) event.get("previousRegistrationKey"), - (String) event.get("nextRegistrationKey"), - (String) event.get("previousVehicleKey"), - (String) event.get("nextVehicleKey"), - longOrNull(event.get("beginBoundaryOdometerKm")), - longOrNull(event.get("endBoundaryOdometerKm")), - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")), - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")), - longOrNull(event.get("geoEvidenceMovementMeters")), - (String) event.get("geoEvidenceMovementCategory"), - beginGeoEvent, - endGeoEvent - )); - } - } - - 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"); - TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent( - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")) - ); - TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent( - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")) - ); - 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("cardAbsentDurationSeconds"), - (Double) event.get("cardAbsentCoveragePercent"), - (String) event.get("previousDrivingSourceIntervalId"), - (String) event.get("nextDrivingSourceIntervalId"), - (String) event.get("previousRegistrationKey"), - (String) event.get("nextRegistrationKey"), - (String) event.get("previousVehicleKey"), - (String) event.get("nextVehicleKey"), - longOrNull(event.get("beginBoundaryOdometerKm")), - longOrNull(event.get("endBoundaryOdometerKm")), - (String) event.get("beginGeoEventId"), - (String) event.get("beginGeoEventDomain"), - offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")), - (Double) event.get("beginLatitude"), - (Double) event.get("beginLongitude"), - longOrNull(event.get("beginGeoDistanceSeconds")), - longOrNull(event.get("beginGeoOdometerKm")), - (String) event.get("endGeoEventId"), - (String) event.get("endGeoEventDomain"), - offsetDateTime(event.get("endGeoOccurredAtEpochSecond")), - (Double) event.get("endLatitude"), - (Double) event.get("endLongitude"), - longOrNull(event.get("endGeoDistanceSeconds")), - longOrNull(event.get("endGeoOdometerKm")), - longOrNull(event.get("geoEvidenceMovementMeters")), - (String) event.get("geoEvidenceMovementCategory"), - beginGeoEvent, - endGeoEvent - )); - } - } - - private void collectPotentialInVehicleTripIntervalEvents( - EventBean[] newData, - List target - ) { - if (newData == null) { - return; - } - for (EventBean event : newData) { - long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); - long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); - target.add(new TachographEsperPotentialInVehicleTripIntervalEvent( - (UUID) event.get("sessionId"), - (String) event.get("driverKey"), - OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), - OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), - (Long) event.get("durationSeconds"), - (String) event.get("registrationKey"), - (String) event.get("vehicleKey"), - (Integer) event.get("containedPotentialInVehicleOvernightStayIntervalCount"), - (Long) event.get("containedPotentialInVehicleOvernightStayDurationSeconds"), - (Long) event.get("containedCardAbsentDurationSeconds"), - OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("firstPotentialInVehicleOvernightStayStartedAtEpochSecond")), ZoneOffset.UTC), - OffsetDateTime.ofInstant(Instant.ofEpochSecond((Long) event.get("lastPotentialInVehicleOvernightStayEndedAtEpochSecond")), ZoneOffset.UTC), - (String) event.get("firstPreviousDrivingSourceIntervalId"), - (String) event.get("lastNextDrivingSourceIntervalId"), - List.of() - )); - } - } - - private List sortDrivingInterruptionIntervals( - List intervals - ) { - return intervals.stream() - .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt) - .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt)) - .toList(); - } - - private List sortVuCardAbsentIntervals( - List intervals - ) { - return intervals.stream() - .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt) - .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt)) - .toList(); - } - - private List sortDailyWeeklyRestCandidateCoverageIntervals( - List intervals - ) { - return deduplicateRestCoverageIntervals(intervals).stream() - .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) - .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) - .toList(); - } - - private List sortPotentialHomeOvernightStayIntervals( - List intervals - ) { - return deduplicatePotentialHomeOvernightStayIntervals(intervals).stream() - .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) - .toList(); - } - - private List sortPotentialInVehicleOvernightStayIntervals( - List intervals - ) { - return deduplicatePotentialInVehicleOvernightStayIntervals(intervals).stream() - .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) - .toList(); - } - - private List deduplicateRestCoverageIntervals( - List intervals - ) { - Map deduplicated = new LinkedHashMap<>(); - for (TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent interval : intervals) { - deduplicated.put(restCoverageIntervalKey( - interval.driverKey(), - interval.startedAt(), - interval.endedAt(), - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId() - ), interval); - } - return new ArrayList<>(deduplicated.values()); - } - - private List deduplicatePotentialHomeOvernightStayIntervals( - List intervals - ) { - Map deduplicated = new LinkedHashMap<>(); - for (TachographEsperPotentialHomeOvernightStayIntervalEvent interval : intervals) { - deduplicated.put(restCoverageIntervalKey( - interval.driverKey(), - interval.startedAt(), - interval.endedAt(), - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId() - ), interval); - } - return new ArrayList<>(deduplicated.values()); - } - - private List deduplicatePotentialInVehicleOvernightStayIntervals( - List intervals - ) { - Map deduplicated = new LinkedHashMap<>(); - for (TachographEsperPotentialInVehicleOvernightStayIntervalEvent interval : intervals) { - deduplicated.put(restCoverageIntervalKey( - interval.driverKey(), - interval.startedAt(), - interval.endedAt(), - interval.previousDrivingSourceIntervalId(), - interval.nextDrivingSourceIntervalId() - ), interval); - } - return new ArrayList<>(deduplicated.values()); - } - - private String restCoverageIntervalKey( - String driverKey, - OffsetDateTime startedAt, - OffsetDateTime endedAt, - String previousDrivingSourceIntervalId, - String nextDrivingSourceIntervalId - ) { - return String.join("|", - driverKey == null ? "" : driverKey, - startedAt == null ? "" : startedAt.toString(), - endedAt == null ? "" : endedAt.toString(), - previousDrivingSourceIntervalId == null ? "" : previousDrivingSourceIntervalId, - nextDrivingSourceIntervalId == null ? "" : nextDrivingSourceIntervalId - ); - } - - private List sortPotentialInVehicleTripIntervals( - List intervals - ) { - return intervals.stream() - .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt) - .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt)) - .toList(); - } - - private String renderDrivingDerivedProjectionEventsEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { - return DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL - + "\n\n" - + renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes); - } - - private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { - return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE - .replace( - "${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}", - Long.toString(Math.max(1, significantDrivingMinutes) * 60L) - ) - .replace( - "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}", - Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L) - ) - .replace( - "${REST_GEO_LOOKBACK_SECONDS}", - Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookbackMinutes()) * 60L) - ) - .replace( - "${REST_GEO_LOOKAHEAD_SECONDS}", - Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookaheadMinutes()) * 60L) - ) - .replace( - "${REST_GEO_STATIONARY_MAX_METERS}", - Integer.toString(Math.max(0, properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters())) - ) - .replace( - "${REST_GEO_MINOR_MOVEMENT_MAX_METERS}", - Integer.toString(Math.max( - properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters(), - properties.getTachographFileSession().getProcessing().getRestCandidateGeoMinorMovementMaxMeters() - )) - ); - } - - private OffsetDateTime offsetDateTime(Object epochSecond) { - if (!(epochSecond instanceof Long value)) { - return null; - } - return OffsetDateTime.ofInstant(Instant.ofEpochSecond(value), ZoneOffset.UTC); - } - - private Long longOrNull(Object value) { - return value instanceof Long number ? number : null; - } - - private TachographEsperGeoEvidenceEvent geoEvidenceEvent( - String eventId, - String eventDomain, - OffsetDateTime occurredAt, - Double latitude, - Double longitude, - Long distanceSeconds, - Long odometerKm - ) { - if (eventId == null - && eventDomain == null - && occurredAt == null - && latitude == null - && longitude == null - && distanceSeconds == null - && odometerKm == null) { - return null; - } - return new TachographEsperGeoEvidenceEvent( - eventId, - eventDomain, - occurredAt, - latitude, - longitude, - distanceSeconds, - odometerKm - ); - } - - 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); - } - } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDerivedProjectionCompatibilityFacade.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDerivedProjectionCompatibilityFacade.java new file mode 100644 index 0000000..48b9047 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDerivedProjectionCompatibilityFacade.java @@ -0,0 +1,236 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeReusableProjectionBuilder; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Tachograph-owned compatibility facade that adapts legacy file-session and event-based callers + * onto the neutral driver working-time projection engine. + */ +@Component +public class TachographDerivedProjectionCompatibilityFacade { + + private final DriverTimelineBuilder driverTimelineBuilder; + private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder; + private final TachographEventTimelineReconstructionHelper reconstructionHelper; + private final EventHubProperties properties; + private final DriverWorkingTimeReusableProjectionBuilder commonBuilder; + + public TachographDerivedProjectionCompatibilityFacade( + DriverTimelineBuilder driverTimelineBuilder, + EventHubProperties properties + ) { + this( + driverTimelineBuilder, + null, + new TachographEventTimelineReconstructionHelper(), + properties, + new DriverWorkingTimeReusableProjectionBuilder(properties) + ); + } + + public TachographDerivedProjectionCompatibilityFacade( + DriverTimelineBuilder driverTimelineBuilder, + RawSourceDriverTimelineEventBuilder rawSourceEventBuilder, + EventHubProperties properties + ) { + this( + driverTimelineBuilder, + rawSourceEventBuilder, + new TachographEventTimelineReconstructionHelper(), + properties, + new DriverWorkingTimeReusableProjectionBuilder(properties) + ); + } + + @Autowired + public TachographDerivedProjectionCompatibilityFacade( + DriverTimelineBuilder driverTimelineBuilder, + RawSourceDriverTimelineEventBuilder rawSourceEventBuilder, + TachographEventTimelineReconstructionHelper reconstructionHelper, + EventHubProperties properties, + DriverWorkingTimeReusableProjectionBuilder commonBuilder + ) { + this.driverTimelineBuilder = driverTimelineBuilder; + this.rawSourceEventBuilder = rawSourceEventBuilder; + this.reconstructionHelper = reconstructionHelper == null + ? new TachographEventTimelineReconstructionHelper() + : reconstructionHelper; + this.properties = properties; + this.commonBuilder = commonBuilder; + } + + public DriverWorkingTimeReusableProjectionBuilder commonBuilder() { + return commonBuilder; + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + TachographFileSession session, + DriverExtractionSession driverSession, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (session == null || driverSession == null || driverSession.driverKey() == null) { + return TachographDriverWorkingTimeAdapter.emptyLegacyBundle(); + } + if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) { + return buildEsperDrivingDerivedProjectionBundleFromEvents( + session.sessionId(), + driverSession.driverKey(), + rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents(), + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); + return buildEsperDrivingDerivedProjectionBundle( + session.sessionId(), + driverSession.driverKey(), + timeline, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + TachographFileSession session, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (session == null || session.driversByKey() == null || session.driversByKey().isEmpty()) { + return TachographDriverWorkingTimeAdapter.emptyLegacyBundle(); + } + + List bundles = session.driversByKey().values().stream() + .map(driverSession -> buildEsperDrivingDerivedProjectionBundle( + session, + driverSession, + significantDrivingMinutes, + minimumRestPeriodMinutes + )) + .toList(); + + return TachographDriverWorkingTimeAdapter.mergeLegacyBundles(bundles); + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + UUID sessionId, + String driverKey, + ResolvedDriverTimeline timeline, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (timeline == null || driverKey == null || driverKey.isBlank()) { + return TachographDriverWorkingTimeAdapter.emptyLegacyBundle(); + } + DriverWorkingTimeDerivedProjectionBundle bundle = commonBuilder.buildDerivedProjectionBundle( + TachographDriverWorkingTimeAdapter.toProcessingInput( + sessionId == null ? new UUID(0L, 0L) : sessionId, + driverKey, + timeline, + null, + null, + significantDrivingMinutes, + minimumRestPeriodMinutes, + List.of() + ) + ); + return TachographDriverWorkingTimeAdapter.toLegacyBundle(bundle); + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents( + List events, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + return buildEsperDrivingDerivedProjectionBundleFromEvents( + null, + null, + events, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents( + UUID fallbackSessionId, + String fallbackDriverKey, + List events, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (fallbackDriverKey == null) { + Map> eventsByDriver = reconstructionHelper.groupEventsByDriverKey(events); + if (eventsByDriver.isEmpty()) { + return TachographDriverWorkingTimeAdapter.emptyLegacyBundle(); + } + if (eventsByDriver.size() > 1) { + return buildEsperDrivingDerivedProjectionBundleFromGroupedEvents( + fallbackSessionId, + eventsByDriver, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + Map.Entry> onlyDriver = eventsByDriver.entrySet().iterator().next(); + fallbackDriverKey = onlyDriver.getKey(); + events = onlyDriver.getValue(); + } + + ResolvedDriverTimeline timeline = reconstructionHelper.reconstructMergedTimelineFromEvents( + fallbackSessionId, + fallbackDriverKey, + events + ); + return buildEsperDrivingDerivedProjectionBundle( + fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId, + fallbackDriverKey, + timeline, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromGroupedEvents( + UUID fallbackSessionId, + Map> eventsByDriver, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + List bundles = eventsByDriver.entrySet().stream() + .map(entry -> buildEsperDrivingDerivedProjectionBundleFromEvents( + fallbackSessionId, + entry.getKey(), + entry.getValue(), + significantDrivingMinutes, + minimumRestPeriodMinutes + )) + .toList(); + + return TachographDriverWorkingTimeAdapter.mergeLegacyBundles(bundles); + } + + private EventHubProperties.DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode() { + return properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode(); + } + + private RawSourceDriverTimelineEventBuilder rawSourceEventBuilder() { + if (rawSourceEventBuilder == null) { + throw new IllegalStateException( + "Driving-derived projection input mode EVENTS requires RawSourceDriverTimelineEventBuilder" + ); + } + return rawSourceEventBuilder; + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDriverWorkingTimeAdapter.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDriverWorkingTimeAdapter.java new file mode 100644 index 0000000..1b62090 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographDriverWorkingTimeAdapter.java @@ -0,0 +1,500 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeGeoEvidenceEvent; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePotentialInVehicleTripInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeRestCoverageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; +import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVuCardAbsentInterval; +import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; +import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; +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.TachographEsperGeoEvidenceEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +public final class TachographDriverWorkingTimeAdapter { + + private TachographDriverWorkingTimeAdapter() { + } + + public static DriverWorkingTimeProcessingInput toProcessingInput( + UUID sessionId, + String driverKey, + ResolvedDriverTimeline timeline, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo, + int significantDrivingMinutes, + int minimumRestPeriodMinutes, + List notes + ) { + return new DriverWorkingTimeProcessingInput( + sessionId, + driverKey, + timeline.sourceKind(), + timeline.loadedFrom(), + timeline.loadedTo(), + requestedFrom, + requestedTo, + significantDrivingMinutes, + minimumRestPeriodMinutes, + safe(timeline.activityIntervals()).stream() + .map(interval -> toActivityInterval(sessionId, driverKey, interval)) + .toList(), + safe(timeline.vehicleUsageIntervals()).stream() + .map(TachographDriverWorkingTimeAdapter::toVehicleUsageInterval) + .toList(), + safe(timeline.supportEvents()).stream() + .map(TachographDriverWorkingTimeAdapter::toSupportEvidenceEvent) + .toList(), + notes + ); + } + + public static DriverWorkingTimeActivityInterval toActivityInterval( + UUID sessionId, + String driverKey, + ResolvedActivityInterval interval + ) { + if (interval == null || interval.from() == null || interval.to() == null) { + return null; + } + return new DriverWorkingTimeActivityInterval( + sessionId, + driverKey, + interval.intervalId(), + interval.activityType(), + interval.slot(), + interval.cardStatus(), + interval.drivingStatus(), + interval.registrationKey(), + interval.vehicleKey(), + interval.sourceKind(), + firstSourceIntervalId(interval), + lastSourceIntervalId(interval), + interval.from(), + interval.to(), + interval.from().toEpochSecond(), + interval.to().toEpochSecond(), + interval.durationSeconds(), + interval.sourceIntervalIds(), + interval.synthetic(), + interval.clippedToRequestedPeriod(), + interval.level() + ); + } + + public static DriverWorkingTimeVehicleUsageInterval toVehicleUsageInterval(ResolvedVehicleUsageInterval interval) { + if (interval == null || interval.from() == null) { + return null; + } + return new DriverWorkingTimeVehicleUsageInterval( + interval.sessionId(), + interval.driverKey(), + interval.intervalId(), + firstSourceIntervalId(interval), + lastSourceIntervalId(interval), + interval.from(), + interval.to(), + interval.from().toEpochSecond(), + interval.to() == null ? null : interval.to().toEpochSecond(), + interval.durationSeconds(), + interval.odometerBeginKm(), + interval.odometerEndKm(), + interval.registrationKey(), + interval.vehicleKey(), + interval.sourceKind(), + interval.sourceIntervalIds() + ); + } + + public static RuntimeSupportEvidenceEvent toSupportEvidenceEvent(ExtractedSupportEvent supportEvent) { + if (supportEvent == null || supportEvent.occurredAt() == null) { + return null; + } + return new RuntimeSupportEvidenceEvent( + supportEvent.eventId(), + null, + null, + supportEvent.eventDomain(), + supportEvent.eventType(), + supportEvent.eventLifecycle(), + supportEvent.driverKey(), + supportEvent.vehicleKey(), + supportEvent.registrationKey(), + supportEvent.occurredAt(), + supportEvent.occurredAt().toEpochSecond(), + supportEvent.latitude(), + supportEvent.longitude(), + supportEvent.country(), + supportEvent.region(), + supportEvent.countryFrom(), + supportEvent.countryTo(), + supportEvent.operation(), + supportEvent.odometerKm(), + supportEvent.avgSpeedKmh(), + supportEvent.maxSpeedKmh(), + java.util.Map.of("rawRecordPath", supportEvent.rawRecordPath()) + ); + } + + public static TachographEsperDrivingDerivedProjectionBundle toLegacyBundle( + DriverWorkingTimeDerivedProjectionBundle bundle + ) { + if (bundle == null) { + return emptyLegacyBundle(); + } + return new TachographEsperDrivingDerivedProjectionBundle( + safe(bundle.drivingInterruptionIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacy).toList(), + safe(bundle.dailyWeeklyRestCandidateIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacy).toList(), + safe(bundle.dailyWeeklyRestCandidateCoverageIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacyCoverage).toList(), + safe(bundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacyCoverage).toList(), + safe(bundle.drivingInterruptionVehicleChangeIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacy).toList(), + safe(bundle.vuCardAbsentIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacy).toList(), + safe(bundle.potentialHomeOvernightStayIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacyPotentialHome).toList(), + safe(bundle.potentialInVehicleOvernightStayIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacyPotentialInVehicle).toList(), + safe(bundle.potentialInVehicleTripIntervals()).stream().map(TachographDriverWorkingTimeAdapter::toLegacy).toList() + ); + } + + public static TachographEsperDrivingDerivedProjectionBundle mergeLegacyBundles( + List bundles + ) { + if (bundles == null || bundles.isEmpty()) { + return emptyLegacyBundle(); + } + + 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<>(); + List potentialInVehicleTripIntervals = new ArrayList<>(); + + for (TachographEsperDrivingDerivedProjectionBundle bundle : safe(bundles)) { + if (bundle == null) { + continue; + } + drivingInterruptionIntervals.addAll(safe(bundle.drivingInterruptionIntervals())); + dailyWeeklyRestCandidateIntervals.addAll(safe(bundle.dailyWeeklyRestCandidateIntervals())); + dailyWeeklyRestCandidateCoverageIntervals.addAll(safe(bundle.dailyWeeklyRestCandidateCoverageIntervals())); + unclassifiedDailyWeeklyRestCandidateCoverageIntervals.addAll(safe(bundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals())); + drivingInterruptionVehicleChangeIntervals.addAll(safe(bundle.drivingInterruptionVehicleChangeIntervals())); + vuCardAbsentIntervals.addAll(safe(bundle.vuCardAbsentIntervals())); + potentialHomeOvernightStayIntervals.addAll(safe(bundle.potentialHomeOvernightStayIntervals())); + potentialInVehicleOvernightStayIntervals.addAll(safe(bundle.potentialInVehicleOvernightStayIntervals())); + potentialInVehicleTripIntervals.addAll(safe(bundle.potentialInVehicleTripIntervals())); + } + + return new TachographEsperDrivingDerivedProjectionBundle( + sortDrivingInterruptionIntervals(drivingInterruptionIntervals), + sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals), + sortCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals), + sortCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals), + sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), + sortVuCardAbsentIntervals(vuCardAbsentIntervals), + sortPotentialHomeIntervals(potentialHomeOvernightStayIntervals), + sortPotentialInVehicleIntervals(potentialInVehicleOvernightStayIntervals), + sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals) + ); + } + + public static TachographEsperDrivingDerivedProjectionBundle emptyLegacyBundle() { + return new TachographEsperDrivingDerivedProjectionBundle( + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + ); + } + + private static TachographEsperDrivingInterruptionIntervalEvent toLegacy( + DriverWorkingTimeDrivingInterruptionInterval value + ) { + return new TachographEsperDrivingInterruptionIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static TachographEsperVuCardAbsentIntervalEvent toLegacy(DriverWorkingTimeVuCardAbsentInterval value) { + return new TachographEsperVuCardAbsentIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.previousUsageIntervalId(), + value.nextUsageIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey() + ); + } + + private static TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent toLegacyCoverage( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static TachographEsperPotentialHomeOvernightStayIntervalEvent toLegacyPotentialHome( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperPotentialHomeOvernightStayIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static TachographEsperPotentialInVehicleOvernightStayIntervalEvent toLegacyPotentialInVehicle( + DriverWorkingTimeRestCoverageInterval value + ) { + return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.cardAbsentDurationSeconds(), + value.cardAbsentCoveragePercent(), + value.previousDrivingSourceIntervalId(), + value.nextDrivingSourceIntervalId(), + value.previousRegistrationKey(), + value.nextRegistrationKey(), + value.previousVehicleKey(), + value.nextVehicleKey(), + value.beginBoundaryOdometerKm(), + value.endBoundaryOdometerKm(), + value.beginGeoEventId(), + value.beginGeoEventDomain(), + value.beginGeoOccurredAt(), + value.beginLatitude(), + value.beginLongitude(), + value.beginGeoDistanceSeconds(), + value.beginGeoOdometerKm(), + value.endGeoEventId(), + value.endGeoEventDomain(), + value.endGeoOccurredAt(), + value.endLatitude(), + value.endLongitude(), + value.endGeoDistanceSeconds(), + value.endGeoOdometerKm(), + value.geoEvidenceMovementMeters(), + value.geoEvidenceMovementCategory(), + toLegacy(value.beginGeoEvent()), + toLegacy(value.endGeoEvent()) + ); + } + + private static TachographEsperPotentialInVehicleTripIntervalEvent toLegacy( + DriverWorkingTimePotentialInVehicleTripInterval value + ) { + return new TachographEsperPotentialInVehicleTripIntervalEvent( + value.sessionId(), + value.driverKey(), + value.startedAt(), + value.endedAt(), + value.durationSeconds(), + value.registrationKey(), + value.vehicleKey(), + value.containedPotentialInVehicleOvernightStayIntervalCount(), + value.containedPotentialInVehicleOvernightStayDurationSeconds(), + value.containedCardAbsentDurationSeconds(), + value.firstPotentialInVehicleOvernightStayStartedAt(), + value.lastPotentialInVehicleOvernightStayEndedAt(), + value.firstPreviousDrivingSourceIntervalId(), + value.lastNextDrivingSourceIntervalId(), + safe(value.potentialInVehicleOvernightStayIntervals()).stream() + .map(TachographDriverWorkingTimeAdapter::toLegacyPotentialInVehicle) + .toList() + ); + } + + private static TachographEsperGeoEvidenceEvent toLegacy(DriverWorkingTimeGeoEvidenceEvent value) { + if (value == null) { + return null; + } + return new TachographEsperGeoEvidenceEvent( + value.eventId(), + value.eventDomain(), + value.occurredAt(), + value.latitude(), + value.longitude(), + value.distanceSeconds(), + value.odometerKm() + ); + } + + private static List sortDrivingInterruptionIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt) + .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt)) + .toList(); + } + + private static List sortVuCardAbsentIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt) + .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt)) + .toList(); + } + + private static List sortCoverageIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt) + .thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt)) + .toList(); + } + + private static List sortPotentialHomeIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) + .toList(); + } + + private static List sortPotentialInVehicleIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt)) + .toList(); + } + + private static List sortPotentialInVehicleTripIntervals( + List intervals + ) { + return safe(intervals).stream() + .sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt)) + .toList(); + } + + private static String firstSourceIntervalId(ResolvedActivityInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().getFirst(); + } + + private static String lastSourceIntervalId(ResolvedActivityInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().getLast(); + } + + private static String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().getFirst(); + } + + private static String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().getLast(); + } + + private static List safe(List values) { + return values == null ? List.of() : values; + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java index 4aa1d4d..4449dfc 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java @@ -4,6 +4,7 @@ import at.procon.eventhub.config.EventHubProperties; import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeDerivedProjectionEngine; import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeReusableProjectionBuilder; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -23,12 +24,24 @@ public class TachographEsperProcessingCore { this.delegate = delegate; } + public TachographEsperProcessingCore( + DriverTimelineBuilder driverTimelineBuilder, + TachographDerivedProjectionCompatibilityFacade compatibilityFacade, + EventHubProperties properties + ) { + this(new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine( + compatibilityFacade.commonBuilder() + ))); + } + public TachographEsperProcessingCore( DriverTimelineBuilder driverTimelineBuilder, DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventHubProperties properties ) { - this(new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine(reusableProjectionBuilder))); + this(new DriverWorkingTimeProcessingCore(new DriverWorkingTimeDerivedProjectionEngine( + reusableProjectionBuilder.commonBuilder() + ))); } public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) { diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingInputAdapter.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingInputAdapter.java index 685fc8b..7e4c1bb 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingInputAdapter.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingInputAdapter.java @@ -1,12 +1,7 @@ package at.procon.eventhub.tachographfilesession.service; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval; import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput; -import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval; -import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent; -import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; -import java.util.List; final class TachographEsperProcessingInputAdapter { @@ -15,57 +10,15 @@ final class TachographEsperProcessingInputAdapter { static DriverWorkingTimeProcessingInput toDriverWorkingTimeInput(TachographEsperProcessingInput input) { ResolvedDriverTimeline timeline = input.timeline(); - String driverKey = input.driverKey(); - return new DriverWorkingTimeProcessingInput( + return TachographDriverWorkingTimeAdapter.toProcessingInput( input.sessionId(), - driverKey, - timeline.sourceKind(), - timeline.loadedFrom(), - timeline.loadedTo(), + input.driverKey(), + timeline, input.requestedFrom(), input.requestedTo(), input.significantDrivingMinutes(), input.minimumRestPeriodMinutes(), - timeline.activityIntervals().stream() - .map(interval -> DriverWorkingTimeActivityInterval.fromResolved(input.sessionId(), driverKey, interval)) - .toList(), - timeline.vehicleUsageIntervals().stream() - .map(DriverWorkingTimeVehicleUsageInterval::fromResolved) - .toList(), - timeline.supportEvents().stream() - .map(TachographEsperProcessingInputAdapter::toSupportEvidenceEvent) - .toList(), input.notes() ); } - - private static RuntimeSupportEvidenceEvent toSupportEvidenceEvent(ExtractedSupportEvent supportEvent) { - if (supportEvent == null || supportEvent.occurredAt() == null) { - return null; - } - return new RuntimeSupportEvidenceEvent( - supportEvent.eventId(), - null, - null, - supportEvent.eventDomain(), - supportEvent.eventType(), - supportEvent.eventLifecycle(), - supportEvent.driverKey(), - supportEvent.vehicleKey(), - supportEvent.registrationKey(), - supportEvent.occurredAt(), - supportEvent.occurredAt().toEpochSecond(), - supportEvent.latitude(), - supportEvent.longitude(), - supportEvent.country(), - supportEvent.region(), - supportEvent.countryFrom(), - supportEvent.countryTo(), - supportEvent.operation(), - supportEvent.odometerKm(), - supportEvent.avgSpeedKmh(), - supportEvent.maxSpeedKmh(), - java.util.Map.of("rawRecordPath", supportEvent.rawRecordPath()) - ); - } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEventTimelineReconstructionHelper.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEventTimelineReconstructionHelper.java new file mode 100644 index 0000000..70cfc23 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEventTimelineReconstructionHelper.java @@ -0,0 +1,194 @@ +package at.procon.eventhub.tachographfilesession.service; + +import com.fasterxml.jackson.databind.JsonNode; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TachographEventTimelineReconstructionHelper { + + private final UnifiedEventTimelineReconstructor timelineReconstructor; + + public TachographEventTimelineReconstructionHelper() { + this(new UnifiedEventTimelineReconstructor()); + } + + @Autowired + public TachographEventTimelineReconstructionHelper( + UnifiedEventTimelineReconstructor timelineReconstructor + ) { + this.timelineReconstructor = timelineReconstructor == null + ? new UnifiedEventTimelineReconstructor() + : timelineReconstructor; + } + + public Map> groupEventsByDriverKey(List events) { + Map> grouped = new LinkedHashMap<>(); + for (EventHubEventDto event : safe(events)) { + String driverKey = eventDriverKey(event); + if (driverKey == null || driverKey.isBlank()) { + continue; + } + grouped.computeIfAbsent(driverKey, ignored -> new ArrayList<>()).add(event); + } + return grouped; + } + + public ResolvedDriverTimeline reconstructMergedTimelineFromEvents( + UUID fallbackSessionId, + String fallbackDriverKey, + List events + ) { + ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct( + fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId, + fallbackDriverKey, + safe(events) + ); + return new ResolvedDriverTimeline( + reconstructed.sourceKind(), + reconstructed.loadedFrom(), + reconstructed.loadedTo(), + mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()), + reconstructed.activityIntervals(), + reconstructed.supportEvents(), + reconstructed.warnings() + ); + } + + private String eventDriverKey(EventHubEventDto event) { + if (event == null) { + return null; + } + JsonNode payload = event.payload(); + JsonNode raw = payload == null || payload.isNull() ? null : (payload.get("raw") == null ? payload : payload.get("raw")); + return firstNonBlank(text(raw, "driverKey"), driverKey(event)); + } + + private String driverKey(EventHubEventDto event) { + if (event.driverRef() == null) { + return null; + } + if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) { + return event.driverRef().driverCard().stableKey(); + } + return event.driverRef().sourceEntityId(); + } + + private List mergeVehicleUsageIntervals( + List intervals, + String sourceKind + ) { + List sorted = safe(intervals).stream() + .sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from) + .thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))) + .toList(); + if (sorted.isEmpty()) { + return List.of(); + } + + List result = new ArrayList<>(); + ResolvedVehicleUsageInterval current = sorted.getFirst(); + List currentSources = new ArrayList<>(current.sourceIntervalIds()); + for (int i = 1; i < sorted.size(); i++) { + ResolvedVehicleUsageInterval next = sorted.get(i); + if (canMergeVehicleUsage(current, next)) { + currentSources.addAll(next.sourceIntervalIds()); + current = ResolvedVehicleUsageInterval.resolved( + current.sessionId(), + current.driverKey(), + current.intervalId() + "+" + next.intervalId(), + current.from(), + mergedTo(current.to(), next.to()), + current.odometerBeginKm(), + mergedOdometerEnd(current, next), + current.registrationKey(), + current.vehicleKey(), + sourceKind, + currentSources + ); + } else { + result.add(current); + current = next; + currentSources = new ArrayList<>(current.sourceIntervalIds()); + } + } + result.add(current); + return result; + } + + private boolean canMergeVehicleUsage(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) { + return Objects.equals(left.registrationKey(), right.registrationKey()) + && Objects.equals(left.vehicleKey(), right.vehicleKey()) + && !right.from().isAfter(mergeBoundary(left.to())); + } + + private Long mergedOdometerEnd( + ResolvedVehicleUsageInterval current, + ResolvedVehicleUsageInterval next + ) { + if (current == null) { + return next == null ? null : next.odometerEndKm(); + } + if (next == null) { + return current.odometerEndKm(); + } + if (current.to() == null || next.to() == null) { + return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm(); + } + if (next.to().isAfter(current.to()) || next.to().isEqual(current.to())) { + return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm(); + } + return current.odometerEndKm() != null ? current.odometerEndKm() : next.odometerEndKm(); + } + + private OffsetDateTime mergedTo(OffsetDateTime left, OffsetDateTime right) { + if (left == null || right == null) { + return null; + } + return left.isAfter(right) ? left : right; + } + + private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) { + return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1); + } + + private String text(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + String text = value.asText(null); + return text == null || text.isBlank() ? null : text; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return null; + } + + private List safe(List values) { + return values == null ? List.of() : values; + } +} 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 7e44494..ca3a33e 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -41,11 +41,26 @@ public class TachographFileSessionProcessingService { private final TachographFileSessionRepository repository; private final DriverTimelineBuilder driverTimelineBuilder; - private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder; private final TachographEsperProcessingCore esperProcessingCore; private final EventHubProperties properties; + public TachographFileSessionProcessingService( + TachographFileSessionRepository repository, + DriverTimelineBuilder driverTimelineBuilder, + TachographDerivedProjectionCompatibilityFacade compatibilityFacade, + EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder, + EventHubProperties properties + ) { + this( + repository, + driverTimelineBuilder, + eventBackedDriverTimelineBuilder, + properties, + new TachographEsperProcessingCore(driverTimelineBuilder, compatibilityFacade, properties) + ); + } + public TachographFileSessionProcessingService( TachographFileSessionRepository repository, DriverTimelineBuilder driverTimelineBuilder, @@ -56,7 +71,6 @@ public class TachographFileSessionProcessingService { this( repository, driverTimelineBuilder, - reusableProjectionBuilder, eventBackedDriverTimelineBuilder, properties, new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties) @@ -67,14 +81,12 @@ public class TachographFileSessionProcessingService { public TachographFileSessionProcessingService( TachographFileSessionRepository repository, DriverTimelineBuilder driverTimelineBuilder, - DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder, EventHubProperties properties, TachographEsperProcessingCore esperProcessingCore ) { this.repository = repository; this.driverTimelineBuilder = driverTimelineBuilder; - this.reusableProjectionBuilder = reusableProjectionBuilder; this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder; this.properties = properties; this.esperProcessingCore = esperProcessingCore; diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCoreParityTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCoreParityTest.java index 0ec76d3..34e7de5 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCoreParityTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCoreParityTest.java @@ -35,14 +35,14 @@ class TachographEsperProcessingCoreParityTest { new EventDetailsFactory(new ObjectMapper()) ); RawSourceDriverTimelineEventBuilder rawSourceEventBuilder = new RawSourceDriverTimelineEventBuilder(intervalEventBuilder); - DriverTimelineReusableProjectionBuilder projectionBuilder = new DriverTimelineReusableProjectionBuilder( + TachographDerivedProjectionCompatibilityFacade projectionFacade = new TachographDerivedProjectionCompatibilityFacade( driverTimelineBuilder, rawSourceEventBuilder, properties ); TachographEsperProcessingCore core = new TachographEsperProcessingCore( driverTimelineBuilder, - projectionBuilder, + projectionFacade, properties ); 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 35bd3aa..06e9818 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -33,7 +33,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -119,7 +119,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -226,7 +226,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -315,7 +315,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -415,7 +415,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -496,7 +496,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -586,7 +586,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder, @@ -660,7 +660,7 @@ class TachographFileSessionProcessingServiceTest { TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, driverTimelineBuilder, - new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()), + new TachographDerivedProjectionCompatibilityFacade(driverTimelineBuilder, new EventHubProperties()), new EventBackedDriverTimelineBuilder( new IntervalBackedDriverTimelineEventBuilder( driverTimelineBuilder,