Generalize runtime working-time processing pipeline
This commit is contained in:
parent
82e2bd0860
commit
f530b68598
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue