Generalize runtime working-time processing pipeline

This commit is contained in:
trifonovt 2026-05-26 17:24:32 +02:00
parent 82e2bd0860
commit f530b68598
16 changed files with 2271 additions and 807 deletions

View File

@ -0,0 +1,120 @@
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;
import java.util.UUID;
public record DriverWorkingTimeActivityInterval(
UUID sessionId,
String driverKey,
String intervalId,
String activityType,
String cardSlot,
String cardStatus,
String drivingStatus,
String registrationKey,
String vehicleKey,
String sourceKind,
String firstSourceIntervalId,
String lastSourceIntervalId,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long startedAtEpochSecond,
long endedAtEpochSecond,
long durationSeconds,
List<String> sourceIntervalIds,
boolean synthetic,
boolean clippedToRequestedPeriod,
String level
) {
public DriverWorkingTimeActivityInterval {
sourceIntervalIds = sourceIntervalIds == null ? List.of() : List.copyOf(sourceIntervalIds);
}
public static DriverWorkingTimeActivityInterval fromMap(Map<String, Object> values) {
if (values == null) {
return null;
}
return new DriverWorkingTimeActivityInterval(
(UUID) values.get("sessionId"),
(String) values.get("driverKey"),
(String) values.get("intervalId"),
(String) values.get("activityType"),
(String) values.get("cardSlot"),
(String) values.get("cardStatus"),
(String) values.get("drivingStatus"),
(String) values.get("registrationKey"),
(String) values.get("vehicleKey"),
(String) values.get("sourceKind"),
(String) values.get("firstSourceIntervalId"),
(String) values.get("lastSourceIntervalId"),
(OffsetDateTime) values.get("startedAt"),
(OffsetDateTime) values.get("endedAt"),
asLong(values.get("startedAtEpochSecond")),
asLong(values.get("endedAtEpochSecond")),
asLong(values.get("durationSeconds")),
castStringList(values.get("sourceIntervalIds")),
asBoolean(values.get("synthetic")),
asBoolean(values.get("clippedToRequestedPeriod")),
(String) values.get("level")
);
}
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<String> castStringList(Object value) {
return value instanceof List<?> list ? (List<String>) list : List.of();
}
private static long asLong(Object value) {
return value instanceof Number number ? number.longValue() : 0L;
}
private static boolean asBoolean(Object value) {
return value instanceof Boolean booleanValue && booleanValue;
}
}

View File

@ -0,0 +1,54 @@
package at.procon.eventhub.processing.driverworkingtime.model;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import java.util.List;
public record DriverWorkingTimeDriverPartition(
String driverKey,
List<EventHubEventDto> driverSeedEvents,
List<EventHubEventDto> attachedVehicleEvidenceEvents,
List<EventHubEventDto> mergedEvents,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals,
RuntimeDriverPartitionDebugDto partitionDebug,
List<RuntimeSupportEvidenceEvent> supportEvidenceEvents,
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization,
List<String> notes,
List<String> warnings
) {
public DriverWorkingTimeDriverPartition {
driverSeedEvents = driverSeedEvents == null ? List.of() : List.copyOf(driverSeedEvents);
attachedVehicleEvidenceEvents = attachedVehicleEvidenceEvents == null ? List.of() : List.copyOf(attachedVehicleEvidenceEvents);
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
supportEvidenceEvents = supportEvidenceEvents == null ? List.of() : List.copyOf(supportEvidenceEvents);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
public DriverWorkingTimeDriverPartition withSupportEvidence(
List<RuntimeSupportEvidenceEvent> newSupportEvidenceEvents,
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug,
List<String> newNotes,
List<String> newWarnings
) {
return new DriverWorkingTimeDriverPartition(
driverKey,
driverSeedEvents,
attachedVehicleEvidenceEvents,
mergedEvents,
discoveredVehicles,
vehicleUsageIntervals,
partitionDebug,
newSupportEvidenceEvents,
normalizationDebug,
newNotes,
newWarnings
);
}
}

View File

@ -0,0 +1,15 @@
package at.procon.eventhub.processing.driverworkingtime.model;
import java.util.Objects;
public record DriverWorkingTimePreparedInput(
String driverKey,
DriverWorkingTimeDriverPartition partition,
DriverWorkingTimeProcessingInput processingInput
) {
public DriverWorkingTimePreparedInput {
driverKey = driverKey == null || driverKey.isBlank() ? null : driverKey.trim();
partition = Objects.requireNonNull(partition, "partition must not be null");
processingInput = Objects.requireNonNull(processingInput, "processingInput must not be null");
}
}

View File

@ -0,0 +1,31 @@
package at.procon.eventhub.processing.driverworkingtime.model;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record DriverWorkingTimeProcessingInput(
UUID sessionId,
String driverKey,
String sourceKind,
OffsetDateTime loadedFrom,
OffsetDateTime loadedTo,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo,
int significantDrivingMinutes,
int minimumRestPeriodMinutes,
List<DriverWorkingTimeActivityInterval> activityIntervals,
List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals,
List<RuntimeSupportEvidenceEvent> supportEvidenceEvents,
List<String> notes
) {
public DriverWorkingTimeProcessingInput {
significantDrivingMinutes = Math.max(1, significantDrivingMinutes);
minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes);
activityIntervals = activityIntervals == null ? List.of() : List.copyOf(activityIntervals);
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
supportEvidenceEvents = supportEvidenceEvents == null ? List.of() : List.copyOf(supportEvidenceEvents);
notes = notes == null ? List.of() : List.copyOf(notes);
}
}

View File

@ -0,0 +1,101 @@
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;
import java.util.UUID;
public record DriverWorkingTimeVehicleUsageInterval(
UUID sessionId,
String driverKey,
String intervalId,
String firstSourceIntervalId,
String lastSourceIntervalId,
OffsetDateTime startedAt,
OffsetDateTime endedAt,
long startedAtEpochSecond,
Long endedAtEpochSecond,
long durationSeconds,
Long odometerBeginKm,
Long odometerEndKm,
String registrationKey,
String vehicleKey,
String sourceKind,
List<String> sourceIntervalIds
) {
public DriverWorkingTimeVehicleUsageInterval {
sourceIntervalIds = sourceIntervalIds == null ? List.of() : List.copyOf(sourceIntervalIds);
}
public static DriverWorkingTimeVehicleUsageInterval fromMap(Map<String, Object> values) {
if (values == null) {
return null;
}
return new DriverWorkingTimeVehicleUsageInterval(
(UUID) values.get("sessionId"),
(String) values.get("driverKey"),
(String) values.get("intervalId"),
(String) values.get("firstSourceIntervalId"),
(String) values.get("lastSourceIntervalId"),
(OffsetDateTime) values.get("startedAt"),
(OffsetDateTime) values.get("endedAt"),
asLong(values.get("startedAtEpochSecond")),
asNullableLong(values.get("endedAtEpochSecond")),
asLong(values.get("durationSeconds")),
asNullableLong(values.get("odometerBeginKm")),
asNullableLong(values.get("odometerEndKm")),
(String) values.get("registrationKey"),
(String) values.get("vehicleKey"),
(String) values.get("sourceKind"),
castStringList(values.get("sourceIntervalIds"))
);
}
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<String> castStringList(Object value) {
return value instanceof List<?> list ? (List<String>) list : List.of();
}
private static long asLong(Object value) {
return value instanceof Number number ? number.longValue() : 0L;
}
private static Long asNullableLong(Object value) {
return value instanceof Number number ? number.longValue() : null;
}
}

View File

@ -1,28 +1,958 @@
package at.procon.eventhub.processing.driverworkingtime.service;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore;
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.*;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.springframework.stereotype.Service;
/**
* Source-neutral driver working-time processing core.
*
* <p>Tachograph file/database data is only one source of the canonical driver activity,
* vehicle-usage, and support-evidence event streams consumed here. The legacy
* TachographEsperProcessingCore delegates to the same processing logic for backward
* compatibility.</p>
* <p>This core consumes canonical driver activity, vehicle-usage, and support-evidence
* timelines. Tachograph files/databases, YellowFox, and future providers should only
* contribute normalized EventHub events or reconstructed timelines; they should not own
* the working-time processing logic.</p>
*/
@Service
public class DriverWorkingTimeProcessingCore {
private final TachographEsperProcessingCore delegate;
private final DriverTimelineBuilder driverTimelineBuilder;
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
private final EventHubProperties properties;
public DriverWorkingTimeProcessingCore(TachographEsperProcessingCore delegate) {
this.delegate = delegate;
public DriverWorkingTimeProcessingCore(
DriverTimelineBuilder driverTimelineBuilder,
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
EventHubProperties properties
) {
this.driverTimelineBuilder = driverTimelineBuilder;
this.reusableProjectionBuilder = reusableProjectionBuilder;
this.properties = properties;
}
public DriverWorkingTimeProcessingResultDto process(TachographEsperProcessingInput input) {
return delegate.processDriverWorkingTime(input);
Objects.requireNonNull(input, "input must not be null");
return process(toSourceNeutralInput(input));
}
public DriverWorkingTimeProcessingResultDto process(DriverWorkingTimeProcessingInput input) {
Objects.requireNonNull(input, "input must not be null");
ResolvedDriverTimeline timeline = toResolvedTimeline(input);
String driverKey = input.driverKey();
OffsetDateTime requestedFrom = input.requestedFrom() == null ? timeline.loadedFrom() : utc(input.requestedFrom());
OffsetDateTime requestedTo = input.requestedTo() == null ? timeline.loadedTo() : utc(input.requestedTo());
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
}
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperActivityIntervalEvents(input.sessionId(), driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperDrivingIntervalEvents(input.sessionId(), driverKey, timeline),
requestedFrom,
requestedTo
);
TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle =
reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
safeSessionId(input.sessionId()),
driverKey,
timeline,
input.significantDrivingMinutes(),
input.minimumRestPeriodMinutes()
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
derivedProjectionBundle.drivingInterruptionIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionIntervals, requestedFrom, requestedTo);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDailyWeeklyRestCandidateIntervals, requestedFrom, requestedTo);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionVehicleChangeIntervals, requestedFrom, requestedTo);
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
derivedProjectionBundle.vuCardAbsentIntervals();
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
clipEsperPotentialInVehicleOvernightStayIntervalEvents(
derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals =
clipEsperPotentialInVehicleTripIntervalEvents(
derivedProjectionBundle.potentialInVehicleTripIntervals(),
potentialInVehicleOvernightStayIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
timeline.supportEvents(),
driverKey,
requestedFrom,
requestedTo
);
return new DriverWorkingTimeProcessingResultDto(
input.sessionId(),
driverKey,
timeline.sourceKind(),
timeline.loadedFrom(),
timeline.loadedTo(),
requestedFrom,
requestedTo,
activityIntervals.size(),
drivingIntervals.size(),
drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
dailyWeeklyRestCandidateCoverageIntervals.size(),
unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
potentialInVehicleOvernightStayIntervals.size(),
potentialInVehicleTripIntervals.size(),
vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(),
supportGeoEvents.size(),
activityIntervals,
drivingIntervals,
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
dailyWeeklyRestCandidateCoverageIntervals,
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
potentialHomeOvernightStayIntervals,
potentialInVehicleOvernightStayIntervals,
potentialInVehicleTripIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
supportGeoEvents,
combinedNotes(input.notes())
);
}
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
return process(input);
}
private DriverWorkingTimeProcessingInput toSourceNeutralInput(TachographEsperProcessingInput input) {
ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
String driverKey = input.driverKey();
return new DriverWorkingTimeProcessingInput(
input.sessionId(),
driverKey,
timeline.sourceKind(),
timeline.loadedFrom(),
timeline.loadedTo(),
input.requestedFrom(),
input.requestedTo(),
input.significantDrivingMinutes(),
input.minimumRestPeriodMinutes(),
safeList(timeline.activityIntervals()).stream()
.map(interval -> DriverWorkingTimeActivityInterval.fromResolved(input.sessionId(), driverKey, interval))
.filter(Objects::nonNull)
.toList(),
safeList(timeline.vehicleUsageIntervals()).stream()
.map(DriverWorkingTimeVehicleUsageInterval::fromResolved)
.filter(Objects::nonNull)
.toList(),
safeList(timeline.supportEvents()).stream()
.map(this::toSupportEvidenceEvent)
.filter(Objects::nonNull)
.toList(),
input.notes()
);
}
private ResolvedDriverTimeline toResolvedTimeline(DriverWorkingTimeProcessingInput input) {
List<ResolvedActivityInterval> activityIntervals = input.activityIntervals().stream()
.map(this::toResolvedActivityInterval)
.filter(Objects::nonNull)
.toList();
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = input.vehicleUsageIntervals().stream()
.map(this::toResolvedVehicleUsageInterval)
.filter(Objects::nonNull)
.toList();
List<ExtractedSupportEvent> 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 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(),
utc(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(),
Map.of("rawRecordPath", supportEvent.rawRecordPath())
);
}
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 OffsetDateTime earliest(
List<ResolvedActivityInterval> activityIntervals,
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
List<ExtractedSupportEvent> 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<ResolvedActivityInterval> activityIntervals,
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
List<ExtractedSupportEvent> 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 <T> List<T> safeList(List<T> values) {
return values == null ? List.of() : values;
}
private List<String> combinedNotes(List<String> extraNotes) {
List<String> notes = new ArrayList<>();
notes.addAll(esperProjectionNotes());
if (extraNotes != null) {
notes.addAll(extraNotes);
}
return List.copyOf(notes);
}
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
List<TachographEsperActivityIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean clipped = interval.clippedToRequestedPeriod()
|| !start.equals(interval.startedAt())
|| !end.equals(interval.endedAt());
return new TachographEsperActivityIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
interval.activityType(),
interval.cardSlot(),
interval.cardStatus(),
interval.drivingStatus(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.sourceIntervalIds(),
interval.synthetic(),
clipped,
interval.level()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
.thenComparing(TachographEsperActivityIntervalEvent::endedAt)
.thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
List<TachographEsperVehicleUsageIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean startClipped = !start.equals(interval.startedAt());
boolean endClipped = !end.equals(interval.endedAt());
return new TachographEsperVehicleUsageIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
start,
end,
Duration.between(start, end).getSeconds(),
startClipped ? null : interval.odometerBeginKm(),
endClipped ? null : interval.odometerEndKm(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
interval.sourceIntervalIds()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperSupportGeoEvent> clipEsperSupportGeoEvents(
List<ExtractedSupportEvent> supportEvents,
String driverKey,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (supportEvents == null || supportEvents.isEmpty() || requestedFrom == null || requestedTo == null) {
return List.of();
}
return supportEvents.stream()
.filter(event -> event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
.filter(event -> event.occurredAt() != null)
.filter(event -> event.latitude() != null && event.longitude() != null)
.filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo))
.map(event -> new TachographEsperSupportGeoEvent(
event.eventId(),
event.driverKey(),
event.occurredAt(),
event.eventDomain(),
event.eventType(),
event.eventLifecycle(),
event.registrationKey(),
event.vehicleKey(),
event.country(),
event.region(),
event.countryFrom(),
event.countryTo(),
event.operation(),
event.latitude(),
event.longitude(),
event.odometerKm(),
event.rawRecordPath()
))
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
.thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperDrivingInterruptionIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperVuCardAbsentIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousUsageIntervalId(),
interval.nextUsageIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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 TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
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(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
.thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> clipEsperPotentialInVehicleOvernightStayIntervalEvents(
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperPotentialInVehicleTripIntervalEvent> clipEsperPotentialInVehicleTripIntervalEvents(
List<TachographEsperPotentialInVehicleTripIntervalEvent> intervals,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
if (intervals == null || intervals.isEmpty()
|| potentialInVehicleOvernightStayIntervals == null || potentialInVehicleOvernightStayIntervals.isEmpty()) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> containedIntervals =
potentialInVehicleOvernightStayIntervals.stream()
.filter(candidate -> tripContainsPotentialInterval(
interval.driverKey(),
interval.registrationKey(),
interval.vehicleKey(),
start,
end,
candidate
))
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
.toList();
if (containedIntervals.isEmpty()) {
return null;
}
TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0);
TachographEsperPotentialInVehicleOvernightStayIntervalEvent last =
containedIntervals.get(containedIntervals.size() - 1);
return new TachographEsperPotentialInVehicleTripIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.registrationKey(),
interval.vehicleKey(),
containedIntervals.size(),
containedIntervals.stream()
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds)
.sum(),
containedIntervals.stream()
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds)
.sum(),
first.startedAt(),
last.endedAt(),
first.previousDrivingSourceIntervalId(),
last.nextDrivingSourceIntervalId(),
containedIntervals
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt))
.toList();
}
private boolean tripContainsPotentialInterval(
String driverKey,
String registrationKey,
String vehicleKey,
OffsetDateTime tripStartedAt,
OffsetDateTime tripEndedAt,
TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate
) {
if (!Objects.equals(driverKey, candidate.driverKey())) {
return false;
}
if (!Objects.equals(registrationKey, candidate.previousRegistrationKey())) {
return false;
}
if (vehicleKey != null && candidate.previousVehicleKey() != null
&& !Objects.equals(vehicleKey, candidate.previousVehicleKey())) {
return false;
}
return !candidate.startedAt().isBefore(tripStartedAt)
&& !candidate.endedAt().isAfter(tripEndedAt);
}
private List<String> esperProjectionNotes() {
return List.of(
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
"Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and card-absent coverage metrics computed from vehicle-usage and VU card-absent overlap.",
"Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.",
"Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.",
"If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.",
"Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
"Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
"Potential in-vehicle trip intervals span from the end of the coverage interval before a same-vehicle in-vehicle-overnight sequence to the start of the first coverage interval after that sequence.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
);
}
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
);
}
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;
}
}

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import java.time.OffsetDateTime;
@ -24,4 +25,18 @@ public record RuntimeVehicleUsageIntervalDebugDto(
interval.sourceKind()
);
}
public static RuntimeVehicleUsageIntervalDebugDto from(DriverWorkingTimeVehicleUsageInterval interval) {
if (interval == null) {
return null;
}
return new RuntimeVehicleUsageIntervalDebugDto(
interval.intervalId(),
interval.startedAt(),
interval.endedAt(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind()
);
}
}

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
@ -66,7 +67,9 @@ public class DriverActivityIntervalsModule implements RuntimeEplModule {
pointEvents
))
));
List<Map<String, Object>> intervals = eplResult.output(OUTPUT_STATEMENT);
List<DriverWorkingTimeActivityInterval> intervals = eplResult.output(OUTPUT_STATEMENT).stream()
.map(DriverWorkingTimeActivityInterval::fromMap)
.toList();
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
metadata.put("inputEventCount", sourceEvents.size());
metadata.put("activityPointEventCount", pointEvents.size());

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
@ -66,7 +67,9 @@ public class DriverVehicleUsageIntervalsModule implements RuntimeEplModule {
pointEvents
))
));
List<Map<String, Object>> intervals = eplResult.output(OUTPUT_STATEMENT);
List<DriverWorkingTimeVehicleUsageInterval> intervals = eplResult.output(OUTPUT_STATEMENT).stream()
.map(DriverWorkingTimeVehicleUsageInterval::fromMap)
.toList();
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
metadata.put("inputEventCount", sourceEvents.size());
metadata.put("vehicleUsagePointEventCount", pointEvents.size());

View File

@ -1,18 +1,128 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class DriverVehicleUsageMergeModule extends AbstractDriverWorkingTimePhaseModule {
public class DriverVehicleUsageMergeModule implements RuntimeProcessingModule {
public DriverVehicleUsageMergeModule() {
super(
DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE,
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Vehicle usage merge",
"Merges adjacent or continuous same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations.",
"JAVA/ESPER",
Set.of("DriverVehicleUsageIntervalInputEvent")
"JAVA",
Set.of("DriverWorkingTimeVehicleUsageInterval")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
Object output = context.previousResults().get(DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS) == null
? null
: context.previousResults().get(DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS).output();
List<DriverWorkingTimeVehicleUsageInterval> intervals = castIntervals(output);
List<DriverWorkingTimeVehicleUsageInterval> merged = merge(intervals);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("inputIntervalCount", intervals.size());
metadata.put("mergedIntervalCount", merged.size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
merged,
metadata,
List.of()
);
}
@SuppressWarnings("unchecked")
private List<DriverWorkingTimeVehicleUsageInterval> castIntervals(Object output) {
return output instanceof List<?> list
? (List<DriverWorkingTimeVehicleUsageInterval>) list
: List.of();
}
private List<DriverWorkingTimeVehicleUsageInterval> merge(List<DriverWorkingTimeVehicleUsageInterval> intervals) {
if (intervals == null || intervals.isEmpty()) {
return List.of();
}
List<DriverWorkingTimeVehicleUsageInterval> sorted = intervals.stream()
.sorted(Comparator.comparing(DriverWorkingTimeVehicleUsageInterval::startedAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DriverWorkingTimeVehicleUsageInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DriverWorkingTimeVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
List<DriverWorkingTimeVehicleUsageInterval> merged = new ArrayList<>();
for (DriverWorkingTimeVehicleUsageInterval next : sorted) {
if (merged.isEmpty()) {
merged.add(next);
continue;
}
DriverWorkingTimeVehicleUsageInterval current = merged.get(merged.size() - 1);
if (canMerge(current, next)) {
merged.set(merged.size() - 1, merge(current, next));
} else {
merged.add(next);
}
}
return List.copyOf(merged);
}
private boolean canMerge(
DriverWorkingTimeVehicleUsageInterval left,
DriverWorkingTimeVehicleUsageInterval right
) {
if (left == null || right == null || left.endedAt() == null || right.startedAt() == null) {
return false;
}
return Objects.equals(left.driverKey(), right.driverKey())
&& Objects.equals(left.registrationKey(), right.registrationKey())
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
&& !right.startedAt().isAfter(left.endedAt().plusSeconds(1));
}
private DriverWorkingTimeVehicleUsageInterval merge(
DriverWorkingTimeVehicleUsageInterval left,
DriverWorkingTimeVehicleUsageInterval right
) {
LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>(left.sourceIntervalIds());
sourceIntervalIds.addAll(right.sourceIntervalIds());
OffsetDateTime mergedEnd = left.endedAt();
if (right.endedAt() != null && (mergedEnd == null || right.endedAt().isAfter(mergedEnd))) {
mergedEnd = right.endedAt();
}
return new DriverWorkingTimeVehicleUsageInterval(
left.sessionId(),
left.driverKey(),
left.intervalId(),
left.firstSourceIntervalId(),
right.lastSourceIntervalId() == null ? left.lastSourceIntervalId() : right.lastSourceIntervalId(),
left.startedAt(),
mergedEnd,
left.startedAtEpochSecond(),
mergedEnd == null ? null : mergedEnd.toEpochSecond(),
mergedEnd == null ? left.durationSeconds() : mergedEnd.toEpochSecond() - left.startedAtEpochSecond(),
left.odometerBeginKm(),
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
left.registrationKey(),
left.vehicleKey(),
left.sourceKind(),
List.copyOf(sourceIntervalIds)
);
}
}

View File

@ -1,23 +1,42 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePreparedInput;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcessingModule {
private final RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService;
private final DriverWorkingTimeProcessingCore workingTimeProcessingCore;
private final RuntimeDriverWorkingTimeScopeProcessingService legacyScopeProcessingService;
@Autowired
public DriverWorkingTimeDerivedProjectionsModule(
DriverWorkingTimeProcessingCore workingTimeProcessingCore
) {
this.workingTimeProcessingCore = workingTimeProcessingCore;
this.legacyScopeProcessingService = null;
}
public DriverWorkingTimeDerivedProjectionsModule(
RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
) {
this.scopeProcessingService = scopeProcessingService;
this.workingTimeProcessingCore = null;
this.legacyScopeProcessingService = scopeProcessingService;
}
@Override
@ -30,21 +49,58 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Driving-derived projections",
"Executes the shared driver working-time pipeline for driving interruptions, rest candidates, card-absence coverage, overnight candidates, and trip candidates.",
"Executes the shared driver working-time core from typed per-driver module outputs for driving interruptions, rest candidates, card-absence coverage, overnight candidates, and trip candidates.",
"ESPER+JAVA",
Set.of("DriverWorkingTimeProcessingResultDto")
Set.of("UnifiedRuntimeDriverWorkingTimeScopeResultDto")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
if (legacyScopeProcessingService != null) {
return executeLegacy(context);
}
UnifiedRuntimeEventBundle broadBundle = runtimeEventBundle(context);
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = scopeProcessingService.processScope(
scopeRequest,
includePartitionDebug
);
Map<String, DriverWorkingTimePreparedInput> preparedInputs = preparedInputs(context);
LinkedHashMap<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>();
for (Map.Entry<String, DriverWorkingTimePreparedInput> entry : preparedInputs.entrySet()) {
DriverWorkingTimePreparedInput preparedInput = entry.getValue();
DriverWorkingTimeProcessingResultDto projection =
workingTimeProcessingCore.process(preparedInput.processingInput());
warnings.addAll(preparedInput.partition().warnings());
UnifiedRuntimeProcessingRequest driverRequest = broadBundle.request().withDriverKey(preparedInput.driverKey());
driverResults.put(preparedInput.driverKey(), new UnifiedRuntimeDerivedProjectionResultDto(
driverRequest,
preparedInput.partition().driverSeedEvents().size(),
preparedInput.partition().discoveredVehicles().size(),
preparedInput.partition().attachedVehicleEvidenceEvents().size(),
preparedInput.partition().mergedEvents().size(),
preparedInput.partition().discoveredVehicles(),
projection,
projection.notes(),
preparedInput.partition().supportEvidenceNormalization(),
preparedInput.partition().partitionDebug()
));
}
List<String> notes = new ArrayList<>(broadBundle.notes());
notes.add("Runtime driver working-time processing used module-to-module dataflow for event assembly, activity intervalization, vehicle-usage intervalization, evidence attachment, support evidence normalization, and final derived projections.");
notes.add("Selected driver partitions: " + driverResults.size() + ".");
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
broadBundle.request(),
broadBundle.mergedEvents().size(),
driverResults.size(),
broadBundle.discoveredVehicles().size(),
broadBundle.discoveredVehicles(),
driverResults,
partitionDebug(driverResults),
notes,
List.copyOf(warnings)
);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("inputEventCount", result.inputEventCount());
metadata.put("selectedDriverCount", result.selectedDriverCount());
@ -59,6 +115,36 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
);
}
private RuntimeProcessingModuleResult executeLegacy(RuntimeProcessingModuleContext context) {
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = legacyScopeProcessingService.processScope(
scopeRequest,
includePartitionDebug
);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("inputEventCount", result.inputEventCount());
metadata.put("selectedDriverCount", result.selectedDriverCount());
metadata.put("discoveredVehicleCount", result.discoveredVehicleCount());
metadata.put("driverResultCount", result.driverResults().size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
result,
metadata,
result.warnings()
);
}
private UnifiedRuntimeEventBundle runtimeEventBundle(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
if (result != null && result.output() instanceof UnifiedRuntimeEventBundle bundle) {
return bundle;
}
throw new IllegalStateException("Module " + moduleKey()
+ " requires previous result " + DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY + ".");
}
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
Object value = context.attributes().get("runtimeScopeApiRequest");
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
@ -67,6 +153,29 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
return context.request().sourceSelection();
}
@SuppressWarnings("unchecked")
private Map<String, DriverWorkingTimePreparedInput> preparedInputs(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result =
context.previousResults().get(DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION);
if (result == null || !(result.output() instanceof Map<?, ?> map)) {
return Map.of();
}
return (Map<String, DriverWorkingTimePreparedInput>) map;
}
private Map<String, at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto> partitionDebug(
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults
) {
LinkedHashMap<String, at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto> debugByDriver =
new LinkedHashMap<>();
driverResults.forEach((driverKey, result) -> {
if (result.partitionDebug() != null) {
debugByDriver.put(driverKey, result.partitionDebug());
}
});
return debugByDriver;
}
private boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) {
Object value = context.attributes().get(key);
if (value instanceof Boolean booleanValue) {

View File

@ -1,18 +1,268 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDriverPartition;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimePreparedInput;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
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.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class SupportEvidenceNormalizationModule extends AbstractDriverWorkingTimePhaseModule {
public class SupportEvidenceNormalizationModule implements RuntimeProcessingModule {
private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
private final EventHubProperties properties;
@Autowired
public SupportEvidenceNormalizationModule(
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer,
EventHubProperties properties
) {
this.supportEvidenceNormalizer = supportEvidenceNormalizer;
this.properties = properties;
}
/** Compatibility constructor for legacy tests/local registries that still delegate the phase. */
public SupportEvidenceNormalizationModule() {
super(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
this.supportEvidenceNormalizer = null;
this.properties = null;
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Support evidence normalization",
"Normalizes mixed-source support evidence for driver working-time processing.",
"Normalizes mixed-source support evidence into typed per-driver working-time inputs for the common processing core.",
"JAVA",
Set.of("RuntimeSupportEvidenceNormalizationDebugDto")
Set.of("Map<String, DriverWorkingTimePreparedInput>")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
if (supportEvidenceNormalizer == null || properties == null) {
return delegatedPlaceholder();
}
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
UnifiedRuntimeProcessingRequest request = scopeRequest.toRuntimeRequest();
Map<String, DriverWorkingTimeDriverPartition> partitions = partitions(context);
List<DriverWorkingTimeActivityInterval> activityIntervals = activityIntervals(context);
LinkedHashMap<String, DriverWorkingTimePreparedInput> preparedInputs = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>();
for (Map.Entry<String, DriverWorkingTimeDriverPartition> entry : partitions.entrySet()) {
String driverKey = entry.getKey();
DriverWorkingTimeDriverPartition partition = entry.getValue();
List<DriverWorkingTimeActivityInterval> driverActivityIntervals = activityIntervals.stream()
.filter(interval -> Objects.equals(driverKey, interval.driverKey()))
.sorted(Comparator.comparing(DriverWorkingTimeActivityInterval::startedAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DriverWorkingTimeActivityInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder())))
.toList();
RuntimeSupportEvidenceNormalizationResult normalizationResult =
supportEvidenceNormalizer.normalizeForTachographDriver(driverKey, partition.mergedEvents());
List<RuntimeSupportEvidenceEvent> supportEvidenceEvents = normalizationResult.normalizedEvents().stream()
.map(event -> supportEvidenceNormalizer.toSupportEvidenceEvent(driverKey, event))
.filter(Objects::nonNull)
.filter(event -> event.occurredAt() != null)
.toList();
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug =
new RuntimeSupportEvidenceNormalizationDebugDto(
normalizationResult.inputEventCount(),
normalizationResult.normalizedSupportEvidenceEventCount(),
normalizationResult.unchangedEventCount(),
normalizationResult.notes()
);
List<String> notes = new ArrayList<>(partition.notes());
notes.addAll(normalizationResult.notes());
notes.add("Support evidence normalization produced " + supportEvidenceEvents.size()
+ " typed support evidence event(s) for driver " + driverKey + ".");
List<String> partitionWarnings = new ArrayList<>(partition.warnings());
warnings.addAll(partitionWarnings);
DriverWorkingTimeDriverPartition normalizedPartition = partition.withSupportEvidence(
supportEvidenceEvents,
normalizationDebug,
notes,
partitionWarnings
);
DriverWorkingTimeProcessingInput processingInput = new DriverWorkingTimeProcessingInput(
runtimeSessionId(request),
driverKey,
resolveSourceKind(normalizedPartition, driverActivityIntervals),
resolveLoadedFrom(normalizedPartition, driverActivityIntervals, supportEvidenceEvents),
resolveLoadedTo(normalizedPartition, driverActivityIntervals, supportEvidenceEvents),
scopeRequest.occurredFrom(),
scopeRequest.occurredTo(),
scopeRequest.significantDrivingMinutes() == null
? properties.getTachographFileSession().getProcessing().getSignificantDrivingMinutes()
: scopeRequest.significantDrivingMinutes(),
scopeRequest.minimumRestPeriodMinutes() == null
? properties.getTachographFileSession().getProcessing().getMinimumRestPeriodMinutes()
: scopeRequest.minimumRestPeriodMinutes(),
driverActivityIntervals,
normalizedPartition.vehicleUsageIntervals(),
supportEvidenceEvents,
notes
);
preparedInputs.put(driverKey, new DriverWorkingTimePreparedInput(
driverKey,
normalizedPartition,
processingInput
));
}
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("selectedDriverCount", preparedInputs.size());
metadata.put("typedSupportEvidenceEventCount", preparedInputs.values().stream()
.mapToInt(value -> value.processingInput().supportEvidenceEvents().size())
.sum());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
Map.copyOf(preparedInputs),
metadata,
List.copyOf(warnings)
);
}
private RuntimeProcessingModuleResult delegatedPlaceholder() {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("executionModel", "delegated");
metadata.put("delegatedTo", DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
metadata.put("note", "This logical module is executed inside the legacy derived projections adapter.");
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
Map.of(),
metadata,
List.of()
);
}
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
Object value = context.attributes().get("runtimeScopeApiRequest");
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
return request;
}
return context.request().sourceSelection();
}
@SuppressWarnings("unchecked")
private Map<String, DriverWorkingTimeDriverPartition> partitions(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT);
if (result == null || !(result.output() instanceof Map<?, ?> map)) {
return Map.of();
}
return (Map<String, DriverWorkingTimeDriverPartition>) map;
}
@SuppressWarnings("unchecked")
private List<DriverWorkingTimeActivityInterval> activityIntervals(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS);
if (result == null || !(result.output() instanceof List<?> list)) {
return List.of();
}
return (List<DriverWorkingTimeActivityInterval>) list;
}
private UUID runtimeSessionId(UnifiedRuntimeProcessingRequest request) {
if (request.compositeSessionId() != null || request.sessionIds().size() > 1) {
return null;
}
return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
}
private String resolveSourceKind(
DriverWorkingTimeDriverPartition partition,
List<DriverWorkingTimeActivityInterval> driverActivityIntervals
) {
for (DriverWorkingTimeVehicleUsageInterval interval : partition.vehicleUsageIntervals()) {
if (interval.sourceKind() != null && !interval.sourceKind().isBlank()) {
return interval.sourceKind();
}
}
for (DriverWorkingTimeActivityInterval interval : driverActivityIntervals) {
if (interval.sourceKind() != null && !interval.sourceKind().isBlank()) {
return interval.sourceKind();
}
}
return "UNIFIED_RUNTIME";
}
private OffsetDateTime resolveLoadedFrom(
DriverWorkingTimeDriverPartition partition,
List<DriverWorkingTimeActivityInterval> driverActivityIntervals,
List<RuntimeSupportEvidenceEvent> supportEvidenceEvents
) {
return earliest(
driverActivityIntervals.stream().map(DriverWorkingTimeActivityInterval::startedAt).toList(),
partition.vehicleUsageIntervals().stream().map(DriverWorkingTimeVehicleUsageInterval::startedAt).toList(),
supportEvidenceEvents.stream().map(RuntimeSupportEvidenceEvent::occurredAt).toList()
);
}
private OffsetDateTime resolveLoadedTo(
DriverWorkingTimeDriverPartition partition,
List<DriverWorkingTimeActivityInterval> driverActivityIntervals,
List<RuntimeSupportEvidenceEvent> supportEvidenceEvents
) {
return latest(
driverActivityIntervals.stream().map(DriverWorkingTimeActivityInterval::endedAt).toList(),
partition.vehicleUsageIntervals().stream().map(DriverWorkingTimeVehicleUsageInterval::endedAt).toList(),
supportEvidenceEvents.stream().map(RuntimeSupportEvidenceEvent::occurredAt).toList()
);
}
@SafeVarargs
private final OffsetDateTime earliest(List<OffsetDateTime>... values) {
OffsetDateTime earliest = null;
for (List<OffsetDateTime> candidates : values) {
for (OffsetDateTime candidate : candidates == null ? List.<OffsetDateTime>of() : candidates) {
if (candidate != null && (earliest == null || candidate.isBefore(earliest))) {
earliest = candidate;
}
}
}
return earliest;
}
@SafeVarargs
private final OffsetDateTime latest(List<OffsetDateTime>... values) {
OffsetDateTime latest = null;
for (List<OffsetDateTime> candidates : values) {
for (OffsetDateTime candidate : candidates == null ? List.<OffsetDateTime>of() : candidates) {
if (candidate != null && (latest == null || candidate.isAfter(latest))) {
latest = candidate;
}
}
}
return latest;
}
}

View File

@ -1,18 +1,310 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDriverPartition;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.service.RuntimeDriverVehicleEvidenceAttachmentService;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class VehicleEvidenceAttachmentModule extends AbstractDriverWorkingTimePhaseModule {
public class VehicleEvidenceAttachmentModule implements RuntimeProcessingModule {
private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;
@Autowired
public VehicleEvidenceAttachmentModule(
RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
) {
this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
}
/** Compatibility constructor for legacy tests/local registries that still delegate the phase. */
public VehicleEvidenceAttachmentModule() {
super(
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
this.vehicleEvidenceAttachmentService = null;
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Vehicle evidence attachment",
"Attaches vehicle-only evidence to driver partitions by overlapping driver vehicle-usage intervals.",
"Partitions the broad runtime scope by driver and attaches vehicle-only evidence using merged vehicle-usage intervals from the common pipeline.",
"JAVA",
Set.of("RuntimeDriverPartitionDebugDto")
Set.of("Map<String, DriverWorkingTimeDriverPartition>")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
if (vehicleEvidenceAttachmentService == null) {
return delegatedPlaceholder();
}
UnifiedRuntimeEventBundle broadBundle = runtimeEventBundle(context);
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
List<DriverWorkingTimeVehicleUsageInterval> mergedVehicleUsageIntervals = mergedVehicleUsageIntervals(context);
LinkedHashMap<String, DriverWorkingTimeDriverPartition> partitions = new LinkedHashMap<>();
LinkedHashMap<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>();
for (String driverKey : selectedDriverKeys(scopeRequest.toRuntimeRequest(), broadBundle.mergedEvents())) {
List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
.filter(event -> Objects.equals(driverKey(event), driverKey))
.toList();
List<DriverWorkingTimeVehicleUsageInterval> driverVehicleUsageIntervals = mergedVehicleUsageIntervals.stream()
.filter(interval -> Objects.equals(driverKey, interval.driverKey()))
.toList();
RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
driverKey,
directDriverEvents,
broadBundle.mergedEvents(),
driverVehicleUsageIntervals,
scopeRequest.expandVehicleEvents() == null || scopeRequest.expandVehicleEvents(),
scopeRequest.vehicleExpansionPaddingMinutes() == null ? 0 : scopeRequest.vehicleExpansionPaddingMinutes(),
includePartitionDebug
);
for (EventHubEventDto attachedEvent : attachmentResult.attachedVehicleEvidenceEvents()) {
attachedVehicleEvidenceByEvent
.computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>())
.add(driverKey);
}
RuntimeDriverPartitionDebugDto partitionDebug = includePartitionDebug ? attachmentResult.toPartitionDebug() : null;
partitions.put(driverKey, new DriverWorkingTimeDriverPartition(
driverKey,
attachmentResult.directDriverEvents(),
attachmentResult.attachedVehicleEvidenceEvents(),
attachmentResult.mergedEvents(),
discoverVehicles(attachmentResult.mergedEvents()),
driverVehicleUsageIntervals,
partitionDebug,
List.of(),
null,
attachmentResult.notes(),
attachmentResult.warnings()
));
warnings.addAll(attachmentResult.warnings());
}
attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> {
if (drivers.size() > 1) {
warnings.add("Vehicle-only event " + eventKey + " was attached to multiple driver partitions "
+ drivers + "; check overlapping vehicle-usage intervals.");
}
});
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("selectedDriverCount", partitions.size());
metadata.put("inputVehicleUsageIntervalCount", mergedVehicleUsageIntervals.size());
metadata.put("attachedVehicleEvidenceEventCount", partitions.values().stream()
.mapToInt(partition -> partition.attachedVehicleEvidenceEvents().size())
.sum());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
Map.copyOf(partitions),
metadata,
List.copyOf(warnings)
);
}
private RuntimeProcessingModuleResult delegatedPlaceholder() {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("executionModel", "delegated");
metadata.put("delegatedTo", DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
metadata.put("note", "This logical module is executed inside the legacy derived projections adapter.");
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
Map.of(),
metadata,
List.of()
);
}
private UnifiedRuntimeEventBundle runtimeEventBundle(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
if (result != null && result.output() instanceof UnifiedRuntimeEventBundle bundle) {
return bundle;
}
throw new IllegalStateException("Module " + moduleKey()
+ " requires previous result " + DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY + ".");
}
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
Object value = context.attributes().get("runtimeScopeApiRequest");
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
return request;
}
return context.request().sourceSelection();
}
@SuppressWarnings("unchecked")
private List<DriverWorkingTimeVehicleUsageInterval> mergedVehicleUsageIntervals(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult result = context.previousResults().get(DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE);
if (result == null || !(result.output() instanceof List<?> list)) {
return List.of();
}
return (List<DriverWorkingTimeVehicleUsageInterval>) list;
}
private LinkedHashSet<String> selectedDriverKeys(
UnifiedRuntimeProcessingRequest request,
List<EventHubEventDto> events
) {
LinkedHashSet<String> allDrivers = discoverDriverKeys(events);
if (request.includeAllDrivers()) {
return allDrivers;
}
LinkedHashSet<String> requested = new LinkedHashSet<>(request.driverKeys());
if (request.driverKey() != null) {
requested.add(request.driverKey());
}
if (requested.isEmpty()) {
return allDrivers;
}
LinkedHashSet<String> selected = new LinkedHashSet<>();
for (String driverKey : allDrivers) {
if (requested.contains(driverKey)) {
selected.add(driverKey);
}
}
selected.addAll(requested);
return selected;
}
private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) {
LinkedHashSet<String> result = new LinkedHashSet<>();
for (EventHubEventDto event : sort(events)) {
String driverKey = driverKey(event);
if (driverKey != null) {
result.add(driverKey);
}
}
return result;
}
private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
List<UnifiedDiscoveredVehicleRef> result = new ArrayList<>();
for (EventHubEventDto event : events == null ? List.<EventHubEventDto>of() : events) {
UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef());
if (candidate == null || !candidate.hasAnyReference()) {
continue;
}
boolean merged = false;
for (int i = 0; i < result.size(); i++) {
UnifiedDiscoveredVehicleRef existing = result.get(i);
if (existing.matches(candidate)) {
result.set(i, existing.merge(candidate));
merged = true;
break;
}
}
if (!merged) {
result.add(candidate);
}
}
result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey));
return List.copyOf(result);
}
private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) {
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
return null;
}
return new UnifiedDiscoveredVehicleRef(
vehicleRef.sourceVehicleEntityId(),
vehicleRef.vin(),
vehicleRef.vehicleRegistration() == null
? null
: vehicleRef.vehicleRegistration().nationNumericCode() == null
? vehicleRef.vehicleRegistration().nation()
: vehicleRef.vehicleRegistration().nationNumericCode().toString(),
vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number()
);
}
private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
return (events == null ? List.<EventHubEventDto>of() : events).stream()
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private String driverKey(EventHubEventDto event) {
if (event == null) {
return null;
}
String rawDriverKey = text(rawPayload(event), "driverKey");
if (rawDriverKey != null) {
return rawDriverKey;
}
DriverRefDto driverRef = event.driverRef();
if (driverRef != null && driverRef.hasAnyReference()) {
return driverRef.stableKey();
}
return null;
}
private JsonNode rawPayload(EventHubEventDto event) {
JsonNode payload = event == null ? null : event.payload();
if (payload == null || payload.isNull()) {
return null;
}
JsonNode raw = payload.get("raw");
return raw == null || raw.isNull() ? payload : raw;
}
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.trim();
}
private String dedupKey(EventHubEventDto event) {
String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
? event.packageInfo().eventSource().stableKey()
: "NO_SOURCE";
return sourceKey + "|" + event.externalSourceEventId();
}
private boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) {
Object value = context.attributes().get(key);
if (value instanceof Boolean booleanValue) {
return booleanValue;
}
return fallback;
}
}

View File

@ -2,6 +2,7 @@ package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
@ -58,9 +59,41 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
boolean attachVehicleOnlyEvents,
int vehicleEvidencePaddingMinutes,
boolean includeDebug
) {
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
null,
driverKey,
directDriverEvents == null ? List.of() : directDriverEvents
);
List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals())
.stream()
.map(DriverWorkingTimeVehicleUsageInterval::fromResolved)
.filter(Objects::nonNull)
.toList();
return attachVehicleEvidence(
driverKey,
directDriverEvents,
runtimeScopeEvents,
vehicleUsageIntervals,
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
includeDebug
);
}
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
String driverKey,
List<EventHubEventDto> directDriverEvents,
List<EventHubEventDto> runtimeScopeEvents,
List<DriverWorkingTimeVehicleUsageInterval> vehicleUsageIntervals,
boolean attachVehicleOnlyEvents,
int vehicleEvidencePaddingMinutes,
boolean includeDebug
) {
List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
List<DriverWorkingTimeVehicleUsageInterval> safeVehicleUsageIntervals =
mergeDriverWorkingTimeVehicleUsageIntervals(vehicleUsageIntervals);
int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);
List<String> notes = new ArrayList<>();
@ -85,10 +118,8 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
);
}
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
? usageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).toList()
? safeVehicleUsageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).filter(Objects::nonNull).toList()
: List.of();
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
? new ArrayList<>(directDriverDecisions(safeDriverEvents))
@ -99,7 +130,8 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
List<EventHubEventDto> attached = new ArrayList<>();
int ignored = 0;
for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
List<DriverWorkingTimeVehicleUsageInterval> matchingIntervals =
matchingUsageIntervals(vehicleEvent, safeVehicleUsageIntervals, paddingMinutes);
if (matchingIntervals.isEmpty()) {
ignored++;
if (includeDebug) {
@ -118,7 +150,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
"ATTACHED_VEHICLE_EVIDENCE",
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
vehicleEvent,
matchingIntervals
intervalIds(matchingIntervals)
));
}
if (matchingIntervals.size() > 1) {
@ -128,13 +160,13 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
}
}
notes.add("Vehicle-only evidence attachment used " + usageIntervals.size()
notes.add("Vehicle-only evidence attachment used " + safeVehicleUsageIntervals.size()
+ " reconstructed vehicle-usage interval(s) for driver " + driverKey + ".");
notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
notes.add("Ignored vehicle-only evidence events: " + ignored + ".");
if (usageIntervals.isEmpty() && !candidateVehicleEvidence.isEmpty()) {
if (safeVehicleUsageIntervals.isEmpty() && !candidateVehicleEvidence.isEmpty()) {
warnings.add("Vehicle-only evidence was available for driver " + driverKey
+ ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
}
@ -144,7 +176,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
safeDriverEvents,
attached,
deduplicateAndSort(safeDriverEvents, attached),
usageIntervals.size(),
safeVehicleUsageIntervals.size(),
candidateVehicleEvidence.size(),
ignored,
usageIntervalDebug,
@ -181,12 +213,8 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
String decision,
String reason,
EventHubEventDto event,
List<ResolvedVehicleUsageInterval> matchingIntervals
List<String> intervalIds
) {
List<String> intervalIds = (matchingIntervals == null ? List.<ResolvedVehicleUsageInterval>of() : matchingIntervals).stream()
.map(ResolvedVehicleUsageInterval::intervalId)
.filter(Objects::nonNull)
.toList();
return new RuntimeVehicleEvidenceAttachmentDecisionDto(
decision,
reason,
@ -198,20 +226,20 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
scopeClassifier.classify(event),
vehicleKeys(event),
intervalIds
intervalIds == null ? List.of() : List.copyOf(intervalIds)
);
}
private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
private List<DriverWorkingTimeVehicleUsageInterval> matchingUsageIntervals(
EventHubEventDto vehicleEvent,
List<ResolvedVehicleUsageInterval> usageIntervals,
List<DriverWorkingTimeVehicleUsageInterval> usageIntervals,
int paddingMinutes
) {
if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
return List.of();
}
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
for (ResolvedVehicleUsageInterval interval : usageIntervals) {
List<DriverWorkingTimeVehicleUsageInterval> result = new ArrayList<>();
for (DriverWorkingTimeVehicleUsageInterval interval : usageIntervals) {
if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
result.add(interval);
}
@ -219,16 +247,20 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
return List.copyOf(result);
}
private boolean timeInside(OffsetDateTime occurredAt, ResolvedVehicleUsageInterval interval, int paddingMinutes) {
if (occurredAt == null || interval == null || interval.from() == null) {
private boolean timeInside(
OffsetDateTime occurredAt,
DriverWorkingTimeVehicleUsageInterval interval,
int paddingMinutes
) {
if (occurredAt == null || interval == null || interval.startedAt() == null) {
return false;
}
OffsetDateTime from = interval.from().minusMinutes(paddingMinutes);
OffsetDateTime to = interval.to() == null ? OffsetDateTime.MAX : interval.to().plusMinutes(paddingMinutes);
OffsetDateTime from = interval.startedAt().minusMinutes(paddingMinutes);
OffsetDateTime to = interval.endedAt() == null ? OffsetDateTime.MAX : interval.endedAt().plusMinutes(paddingMinutes);
return !occurredAt.isBefore(from) && !occurredAt.isAfter(to);
}
private boolean matchesVehicle(EventHubEventDto event, ResolvedVehicleUsageInterval interval) {
private boolean matchesVehicle(EventHubEventDto event, DriverWorkingTimeVehicleUsageInterval interval) {
if (event == null || interval == null) {
return false;
}
@ -262,7 +294,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
return Set.copyOf(result);
}
private Set<String> vehicleKeys(ResolvedVehicleUsageInterval interval) {
private Set<String> vehicleKeys(DriverWorkingTimeVehicleUsageInterval interval) {
LinkedHashSet<String> result = new LinkedHashSet<>();
add(result, interval.vehicleKey());
add(result, interval.registrationKey());
@ -275,6 +307,13 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
return Set.copyOf(result);
}
private List<String> intervalIds(List<DriverWorkingTimeVehicleUsageInterval> intervals) {
return (intervals == null ? List.<DriverWorkingTimeVehicleUsageInterval>of() : intervals).stream()
.map(DriverWorkingTimeVehicleUsageInterval::intervalId)
.filter(Objects::nonNull)
.toList();
}
private void add(Set<String> keys, String value) {
if (value != null && !value.isBlank()) {
keys.add(value.trim());
@ -343,6 +382,77 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
);
}
private List<DriverWorkingTimeVehicleUsageInterval> mergeDriverWorkingTimeVehicleUsageIntervals(
List<DriverWorkingTimeVehicleUsageInterval> intervals
) {
if (intervals == null || intervals.isEmpty()) {
return List.of();
}
List<DriverWorkingTimeVehicleUsageInterval> sorted = intervals.stream()
.filter(Objects::nonNull)
.sorted(Comparator.comparing(DriverWorkingTimeVehicleUsageInterval::startedAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DriverWorkingTimeVehicleUsageInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DriverWorkingTimeVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
List<DriverWorkingTimeVehicleUsageInterval> merged = new ArrayList<>();
for (DriverWorkingTimeVehicleUsageInterval next : sorted) {
if (merged.isEmpty()) {
merged.add(next);
continue;
}
DriverWorkingTimeVehicleUsageInterval current = merged.get(merged.size() - 1);
if (canMerge(current, next)) {
merged.set(merged.size() - 1, merge(current, next));
} else {
merged.add(next);
}
}
return List.copyOf(merged);
}
private boolean canMerge(
DriverWorkingTimeVehicleUsageInterval left,
DriverWorkingTimeVehicleUsageInterval right
) {
if (left == null || right == null || left.endedAt() == null || right.startedAt() == null) {
return false;
}
return Objects.equals(left.driverKey(), right.driverKey())
&& Objects.equals(left.registrationKey(), right.registrationKey())
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
&& !right.startedAt().isAfter(left.endedAt().plusSeconds(1));
}
private DriverWorkingTimeVehicleUsageInterval merge(
DriverWorkingTimeVehicleUsageInterval left,
DriverWorkingTimeVehicleUsageInterval right
) {
LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>(left.sourceIntervalIds());
sourceIntervalIds.addAll(right.sourceIntervalIds());
OffsetDateTime end = left.endedAt();
if (right.endedAt() != null && (end == null || right.endedAt().isAfter(end))) {
end = right.endedAt();
}
return new DriverWorkingTimeVehicleUsageInterval(
left.sessionId(),
left.driverKey(),
left.intervalId(),
left.firstSourceIntervalId(),
right.lastSourceIntervalId() == null ? left.lastSourceIntervalId() : right.lastSourceIntervalId(),
left.startedAt(),
end,
left.startedAtEpochSecond(),
end == null ? null : end.toEpochSecond(),
end == null ? left.durationSeconds() : end.toEpochSecond() - left.startedAtEpochSecond(),
left.odometerBeginKm(),
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
left.registrationKey(),
left.vehicleKey(),
left.sourceKind(),
List.copyOf(sourceIntervalIds)
);
}
private List<EventHubEventDto> deduplicateAndSort(
List<EventHubEventDto> directDriverEvents,
List<EventHubEventDto> vehicleEvidenceEvents

View File

@ -6,7 +6,11 @@ import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
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.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;
@ -26,7 +30,6 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsen
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@ -63,7 +66,7 @@ public class UnifiedRuntimeDerivedProjectionService {
driverTimelineBuilder,
reusableProjectionBuilder,
properties,
new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
new DriverWorkingTimeProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties),
supportEvidenceNormalizer
);
}
@ -141,17 +144,31 @@ public class UnifiedRuntimeDerivedProjectionService {
notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
}
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents(
DriverWorkingTimeProcessingInput processingInput = new DriverWorkingTimeProcessingInput(
runtimeSessionId(request),
driverKey,
timeline,
normalizedEvents,
timeline.sourceKind(),
timeline.loadedFrom(),
timeline.loadedTo(),
requestedFrom,
requestedTo,
significantDrivingMinutes,
minimumRestPeriodMinutes,
timeline.activityIntervals().stream()
.map(interval -> DriverWorkingTimeActivityInterval.fromResolved(runtimeSessionId(request), driverKey, interval))
.filter(Objects::nonNull)
.toList(),
timeline.vehicleUsageIntervals().stream()
.map(DriverWorkingTimeVehicleUsageInterval::fromResolved)
.filter(Objects::nonNull)
.toList(),
timeline.supportEvents().stream()
.map(this::toSupportEvidenceEvent)
.filter(Objects::nonNull)
.toList(),
notes
));
);
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(processingInput);
notes = projection.notes();
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
@ -185,6 +202,36 @@ public class UnifiedRuntimeDerivedProjectionService {
return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
}
private 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())
);
}
private String resolveDriverKey(
UnifiedRuntimeProcessingRequest request,
List<EventHubEventDto> events

View File

@ -2,765 +2,39 @@ package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.*;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @deprecated Use {@link DriverWorkingTimeProcessingCore}. This class remains as a
* compatibility adapter for existing tachograph file-session callers.
*/
@Service
@Deprecated(forRemoval = false)
public class TachographEsperProcessingCore {
private final DriverTimelineBuilder driverTimelineBuilder;
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
private final EventHubProperties properties;
private final DriverWorkingTimeProcessingCore delegate;
@Autowired
public TachographEsperProcessingCore(DriverWorkingTimeProcessingCore delegate) {
this.delegate = delegate;
}
public TachographEsperProcessingCore(
DriverTimelineBuilder driverTimelineBuilder,
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
EventHubProperties properties
) {
this.driverTimelineBuilder = driverTimelineBuilder;
this.reusableProjectionBuilder = reusableProjectionBuilder;
this.properties = properties;
this(new DriverWorkingTimeProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties));
}
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(processDriverWorkingTime(input));
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(delegate.process(input));
}
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
Objects.requireNonNull(input, "input must not be null");
ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
String driverKey = input.driverKey();
OffsetDateTime requestedFrom = input.requestedFrom() == null ? timeline.loadedFrom() : utc(input.requestedFrom());
OffsetDateTime requestedTo = input.requestedTo() == null ? timeline.loadedTo() : utc(input.requestedTo());
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
}
int significantDrivingMinutes = Math.max(1, input.significantDrivingMinutes());
int minimumRestPeriodMinutes = Math.max(1, input.minimumRestPeriodMinutes());
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperActivityIntervalEvents(input.sessionId(), driverKey, timeline),
requestedFrom,
requestedTo
);
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
driverTimelineBuilder.buildEsperDrivingIntervalEvents(input.sessionId(), driverKey, timeline),
requestedFrom,
requestedTo
);
TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle = buildDerivedProjection(
input,
timeline,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
derivedProjectionBundle.drivingInterruptionIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionIntervals, requestedFrom, requestedTo);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDailyWeeklyRestCandidateIntervals, requestedFrom, requestedTo);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionVehicleChangeIntervals, requestedFrom, requestedTo);
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
derivedProjectionBundle.vuCardAbsentIntervals();
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
clipEsperPotentialInVehicleOvernightStayIntervalEvents(
derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
rawVuCardAbsentIntervals,
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals =
clipEsperPotentialInVehicleTripIntervalEvents(
derivedProjectionBundle.potentialInVehicleTripIntervals(),
potentialInVehicleOvernightStayIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
rawVehicleUsageIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo
);
List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
timeline.supportEvents(),
driverKey,
requestedFrom,
requestedTo
);
return new DriverWorkingTimeProcessingResultDto(
input.sessionId(),
driverKey,
timeline.sourceKind(),
timeline.loadedFrom(),
timeline.loadedTo(),
requestedFrom,
requestedTo,
activityIntervals.size(),
drivingIntervals.size(),
drivingInterruptionIntervals.size(),
drivingInterruptionVehicleChangeIntervals.size(),
dailyWeeklyRestCandidateIntervals.size(),
dailyWeeklyRestCandidateCoverageIntervals.size(),
unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
potentialHomeOvernightStayIntervals.size(),
potentialInVehicleOvernightStayIntervals.size(),
potentialInVehicleTripIntervals.size(),
vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(),
supportGeoEvents.size(),
activityIntervals,
drivingIntervals,
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
dailyWeeklyRestCandidateCoverageIntervals,
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
potentialHomeOvernightStayIntervals,
potentialInVehicleOvernightStayIntervals,
potentialInVehicleTripIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
supportGeoEvents,
combinedNotes(input.notes())
);
}
private TachographEsperDrivingDerivedProjectionBundle buildDerivedProjection(
TachographEsperProcessingInput input,
ResolvedDriverTimeline timeline,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
if (input.forceEventInput() || input.hasEventInputEvents()) {
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(
input.sessionId(),
input.driverKey(),
input.eventInputEvents(),
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
if (properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode()
== EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS
&& input.session() != null
&& input.driverSession() != null) {
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
input.session(),
input.driverSession(),
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
input.sessionId(),
input.driverKey(),
timeline,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
private List<String> combinedNotes(List<String> extraNotes) {
List<String> notes = new ArrayList<>();
notes.addAll(esperProjectionNotes());
if (extraNotes != null) {
notes.addAll(extraNotes);
}
return List.copyOf(notes);
}
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
List<TachographEsperActivityIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean clipped = interval.clippedToRequestedPeriod()
|| !start.equals(interval.startedAt())
|| !end.equals(interval.endedAt());
return new TachographEsperActivityIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
interval.activityType(),
interval.cardSlot(),
interval.cardStatus(),
interval.drivingStatus(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.sourceIntervalIds(),
interval.synthetic(),
clipped,
interval.level()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
.thenComparing(TachographEsperActivityIntervalEvent::endedAt)
.thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
List<TachographEsperVehicleUsageIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
boolean startClipped = !start.equals(interval.startedAt());
boolean endClipped = !end.equals(interval.endedAt());
return new TachographEsperVehicleUsageIntervalEvent(
interval.sessionId(),
interval.driverKey(),
interval.intervalId(),
start,
end,
Duration.between(start, end).getSeconds(),
startClipped ? null : interval.odometerBeginKm(),
endClipped ? null : interval.odometerEndKm(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind(),
interval.sourceIntervalIds()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperSupportGeoEvent> clipEsperSupportGeoEvents(
List<ExtractedSupportEvent> supportEvents,
String driverKey,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (supportEvents == null || supportEvents.isEmpty() || requestedFrom == null || requestedTo == null) {
return List.of();
}
return supportEvents.stream()
.filter(event -> event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
.filter(event -> event.occurredAt() != null)
.filter(event -> event.latitude() != null && event.longitude() != null)
.filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo))
.map(event -> new TachographEsperSupportGeoEvent(
event.eventId(),
event.driverKey(),
event.occurredAt(),
event.eventDomain(),
event.eventType(),
event.eventLifecycle(),
event.registrationKey(),
event.vehicleKey(),
event.country(),
event.region(),
event.countryFrom(),
event.countryTo(),
event.operation(),
event.latitude(),
event.longitude(),
event.odometerKm(),
event.rawRecordPath()
))
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
.thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperDrivingInterruptionIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousDrivingSourceIntervalId(),
interval.nextDrivingSourceIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
return new TachographEsperVuCardAbsentIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.previousUsageIntervalId(),
interval.nextUsageIntervalId(),
interval.previousRegistrationKey(),
interval.nextRegistrationKey(),
interval.previousVehicleKey(),
interval.nextVehicleKey()
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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 TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
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(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
.thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> clipEsperPotentialInVehicleOvernightStayIntervalEvents(
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals,
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
List<TachographEsperVehicleUsageIntervalEvent> 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<TachographEsperPotentialInVehicleTripIntervalEvent> clipEsperPotentialInVehicleTripIntervalEvents(
List<TachographEsperPotentialInVehicleTripIntervalEvent> intervals,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (requestedFrom == null || requestedTo == null) {
return List.of();
}
if (intervals == null || intervals.isEmpty()
|| potentialInVehicleOvernightStayIntervals == null || potentialInVehicleOvernightStayIntervals.isEmpty()) {
return List.of();
}
return intervals.stream()
.map(interval -> {
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
OffsetDateTime end = min(interval.endedAt(), requestedTo);
if (!end.isAfter(start)) {
return null;
}
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> containedIntervals =
potentialInVehicleOvernightStayIntervals.stream()
.filter(candidate -> tripContainsPotentialInterval(
interval.driverKey(),
interval.registrationKey(),
interval.vehicleKey(),
start,
end,
candidate
))
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
.toList();
if (containedIntervals.isEmpty()) {
return null;
}
TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0);
TachographEsperPotentialInVehicleOvernightStayIntervalEvent last =
containedIntervals.get(containedIntervals.size() - 1);
return new TachographEsperPotentialInVehicleTripIntervalEvent(
interval.sessionId(),
interval.driverKey(),
start,
end,
Duration.between(start, end).getSeconds(),
interval.registrationKey(),
interval.vehicleKey(),
containedIntervals.size(),
containedIntervals.stream()
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds)
.sum(),
containedIntervals.stream()
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds)
.sum(),
first.startedAt(),
last.endedAt(),
first.previousDrivingSourceIntervalId(),
last.nextDrivingSourceIntervalId(),
containedIntervals
);
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt)
.thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt))
.toList();
}
private boolean tripContainsPotentialInterval(
String driverKey,
String registrationKey,
String vehicleKey,
OffsetDateTime tripStartedAt,
OffsetDateTime tripEndedAt,
TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate
) {
if (!Objects.equals(driverKey, candidate.driverKey())) {
return false;
}
if (!Objects.equals(registrationKey, candidate.previousRegistrationKey())) {
return false;
}
if (vehicleKey != null && candidate.previousVehicleKey() != null
&& !Objects.equals(vehicleKey, candidate.previousVehicleKey())) {
return false;
}
return !candidate.startedAt().isBefore(tripStartedAt)
&& !candidate.endedAt().isAfter(tripEndedAt);
}
private List<String> esperProjectionNotes() {
return List.of(
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
"Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and card-absent coverage metrics computed from vehicle-usage and VU card-absent overlap.",
"Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.",
"Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.",
"If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.",
"Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
"Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
"Potential in-vehicle trip intervals span from the end of the coverage interval before a same-vehicle in-vehicle-overnight sequence to the start of the first coverage interval after that sequence.",
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
);
}
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
);
}
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 delegate.process(input);
}
}