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;
|
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.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 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;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source-neutral driver working-time processing core.
|
* Source-neutral driver working-time processing core.
|
||||||
*
|
*
|
||||||
* <p>Tachograph file/database data is only one source of the canonical driver activity,
|
* <p>This core consumes canonical driver activity, vehicle-usage, and support-evidence
|
||||||
* vehicle-usage, and support-evidence event streams consumed here. The legacy
|
* timelines. Tachograph files/databases, YellowFox, and future providers should only
|
||||||
* TachographEsperProcessingCore delegates to the same processing logic for backward
|
* contribute normalized EventHub events or reconstructed timelines; they should not own
|
||||||
* compatibility.</p>
|
* the working-time processing logic.</p>
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class DriverWorkingTimeProcessingCore {
|
public class DriverWorkingTimeProcessingCore {
|
||||||
|
|
||||||
private final TachographEsperProcessingCore delegate;
|
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||||
|
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||||
|
private final EventHubProperties properties;
|
||||||
|
|
||||||
public DriverWorkingTimeProcessingCore(TachographEsperProcessingCore delegate) {
|
public DriverWorkingTimeProcessingCore(
|
||||||
this.delegate = delegate;
|
DriverTimelineBuilder driverTimelineBuilder,
|
||||||
|
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||||
|
EventHubProperties properties
|
||||||
|
) {
|
||||||
|
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||||
|
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||||
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DriverWorkingTimeProcessingResultDto process(TachographEsperProcessingInput input) {
|
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;
|
package at.procon.eventhub.processing.dto;
|
||||||
|
|
||||||
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
|
@ -24,4 +25,18 @@ public record RuntimeVehicleUsageIntervalDebugDto(
|
||||||
interval.sourceKind()
|
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;
|
package at.procon.eventhub.processing.eventprocessing.module;
|
||||||
|
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
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.DriverWorkingTimeEplEventMapper;
|
||||||
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
|
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
|
||||||
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
|
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
|
||||||
|
|
@ -66,7 +67,9 @@ public class DriverActivityIntervalsModule implements RuntimeEplModule {
|
||||||
pointEvents
|
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());
|
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
|
||||||
metadata.put("inputEventCount", sourceEvents.size());
|
metadata.put("inputEventCount", sourceEvents.size());
|
||||||
metadata.put("activityPointEventCount", pointEvents.size());
|
metadata.put("activityPointEventCount", pointEvents.size());
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package at.procon.eventhub.processing.eventprocessing.module;
|
package at.procon.eventhub.processing.eventprocessing.module;
|
||||||
|
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
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.DriverWorkingTimeEplEventMapper;
|
||||||
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
|
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
|
||||||
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
|
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
|
||||||
|
|
@ -66,7 +67,9 @@ public class DriverVehicleUsageIntervalsModule implements RuntimeEplModule {
|
||||||
pointEvents
|
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());
|
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
|
||||||
metadata.put("inputEventCount", sourceEvents.size());
|
metadata.put("inputEventCount", sourceEvents.size());
|
||||||
metadata.put("vehicleUsagePointEventCount", pointEvents.size());
|
metadata.put("vehicleUsagePointEventCount", pointEvents.size());
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,128 @@
|
||||||
package at.procon.eventhub.processing.eventprocessing.module;
|
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 java.util.Set;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DriverVehicleUsageMergeModule extends AbstractDriverWorkingTimePhaseModule {
|
public class DriverVehicleUsageMergeModule implements RuntimeProcessingModule {
|
||||||
|
|
||||||
public DriverVehicleUsageMergeModule() {
|
@Override
|
||||||
super(
|
public String moduleKey() {
|
||||||
DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE,
|
return DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuntimeProcessingModuleDescriptorDto descriptor() {
|
||||||
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
|
moduleKey(),
|
||||||
"Vehicle usage merge",
|
"Vehicle usage merge",
|
||||||
"Merges adjacent or continuous same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations.",
|
"Merges adjacent or continuous same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations.",
|
||||||
"JAVA/ESPER",
|
"JAVA",
|
||||||
Set.of("DriverVehicleUsageIntervalInputEvent")
|
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;
|
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.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
|
||||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
|
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.service.RuntimeDriverWorkingTimeScopeProcessingService;
|
||||||
|
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcessingModule {
|
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(
|
public DriverWorkingTimeDerivedProjectionsModule(
|
||||||
RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
|
RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
|
||||||
) {
|
) {
|
||||||
this.scopeProcessingService = scopeProcessingService;
|
this.workingTimeProcessingCore = null;
|
||||||
|
this.legacyScopeProcessingService = scopeProcessingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -30,21 +49,58 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
|
||||||
return new RuntimeProcessingModuleDescriptorDto(
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
"Driving-derived projections",
|
"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",
|
"ESPER+JAVA",
|
||||||
Set.of("DriverWorkingTimeProcessingResultDto")
|
Set.of("UnifiedRuntimeDriverWorkingTimeScopeResultDto")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
|
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
|
||||||
|
if (legacyScopeProcessingService != null) {
|
||||||
|
return executeLegacy(context);
|
||||||
|
}
|
||||||
|
UnifiedRuntimeEventBundle broadBundle = runtimeEventBundle(context);
|
||||||
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
|
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
|
||||||
boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
|
Map<String, DriverWorkingTimePreparedInput> preparedInputs = preparedInputs(context);
|
||||||
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = scopeProcessingService.processScope(
|
|
||||||
scopeRequest,
|
|
||||||
includePartitionDebug
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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<>();
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
metadata.put("inputEventCount", result.inputEventCount());
|
metadata.put("inputEventCount", result.inputEventCount());
|
||||||
metadata.put("selectedDriverCount", result.selectedDriverCount());
|
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) {
|
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
|
||||||
Object value = context.attributes().get("runtimeScopeApiRequest");
|
Object value = context.attributes().get("runtimeScopeApiRequest");
|
||||||
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
|
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
|
||||||
|
|
@ -67,6 +153,29 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
|
||||||
return context.request().sourceSelection();
|
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) {
|
private boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) {
|
||||||
Object value = context.attributes().get(key);
|
Object value = context.attributes().get(key);
|
||||||
if (value instanceof Boolean booleanValue) {
|
if (value instanceof Boolean booleanValue) {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,268 @@
|
||||||
package at.procon.eventhub.processing.eventprocessing.module;
|
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.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@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() {
|
public SupportEvidenceNormalizationModule() {
|
||||||
super(
|
this.supportEvidenceNormalizer = null;
|
||||||
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
|
this.properties = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String moduleKey() {
|
||||||
|
return DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuntimeProcessingModuleDescriptorDto descriptor() {
|
||||||
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
|
moduleKey(),
|
||||||
"Support evidence normalization",
|
"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",
|
"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;
|
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 java.util.Set;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@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() {
|
public VehicleEvidenceAttachmentModule() {
|
||||||
super(
|
this.vehicleEvidenceAttachmentService = null;
|
||||||
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String moduleKey() {
|
||||||
|
return DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuntimeProcessingModuleDescriptorDto descriptor() {
|
||||||
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
|
moduleKey(),
|
||||||
"Vehicle evidence attachment",
|
"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",
|
"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.EventHubEventDto;
|
||||||
import at.procon.eventhub.dto.VehicleRefDto;
|
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.RuntimeVehicleEvidenceAttachmentDecisionDto;
|
||||||
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
|
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
|
||||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
|
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
|
||||||
|
|
@ -58,9 +59,41 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
boolean attachVehicleOnlyEvents,
|
boolean attachVehicleOnlyEvents,
|
||||||
int vehicleEvidencePaddingMinutes,
|
int vehicleEvidencePaddingMinutes,
|
||||||
boolean includeDebug
|
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> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
|
||||||
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
|
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
|
||||||
|
List<DriverWorkingTimeVehicleUsageInterval> safeVehicleUsageIntervals =
|
||||||
|
mergeDriverWorkingTimeVehicleUsageIntervals(vehicleUsageIntervals);
|
||||||
int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);
|
int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);
|
||||||
|
|
||||||
List<String> notes = new ArrayList<>();
|
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
|
List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
|
||||||
? usageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).toList()
|
? safeVehicleUsageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).filter(Objects::nonNull).toList()
|
||||||
: List.of();
|
: List.of();
|
||||||
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
|
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
|
||||||
? new ArrayList<>(directDriverDecisions(safeDriverEvents))
|
? new ArrayList<>(directDriverDecisions(safeDriverEvents))
|
||||||
|
|
@ -99,7 +130,8 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
List<EventHubEventDto> attached = new ArrayList<>();
|
List<EventHubEventDto> attached = new ArrayList<>();
|
||||||
int ignored = 0;
|
int ignored = 0;
|
||||||
for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
|
for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
|
||||||
List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
|
List<DriverWorkingTimeVehicleUsageInterval> matchingIntervals =
|
||||||
|
matchingUsageIntervals(vehicleEvent, safeVehicleUsageIntervals, paddingMinutes);
|
||||||
if (matchingIntervals.isEmpty()) {
|
if (matchingIntervals.isEmpty()) {
|
||||||
ignored++;
|
ignored++;
|
||||||
if (includeDebug) {
|
if (includeDebug) {
|
||||||
|
|
@ -118,7 +150,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
"ATTACHED_VEHICLE_EVIDENCE",
|
"ATTACHED_VEHICLE_EVIDENCE",
|
||||||
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
|
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
|
||||||
vehicleEvent,
|
vehicleEvent,
|
||||||
matchingIntervals
|
intervalIds(matchingIntervals)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (matchingIntervals.size() > 1) {
|
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 + ".");
|
+ " reconstructed vehicle-usage interval(s) for driver " + driverKey + ".");
|
||||||
notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
|
notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
|
||||||
notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
|
notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
|
||||||
notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
|
notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
|
||||||
notes.add("Ignored vehicle-only evidence events: " + ignored + ".");
|
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
|
warnings.add("Vehicle-only evidence was available for driver " + driverKey
|
||||||
+ ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
|
+ ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +176,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
safeDriverEvents,
|
safeDriverEvents,
|
||||||
attached,
|
attached,
|
||||||
deduplicateAndSort(safeDriverEvents, attached),
|
deduplicateAndSort(safeDriverEvents, attached),
|
||||||
usageIntervals.size(),
|
safeVehicleUsageIntervals.size(),
|
||||||
candidateVehicleEvidence.size(),
|
candidateVehicleEvidence.size(),
|
||||||
ignored,
|
ignored,
|
||||||
usageIntervalDebug,
|
usageIntervalDebug,
|
||||||
|
|
@ -181,12 +213,8 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
String decision,
|
String decision,
|
||||||
String reason,
|
String reason,
|
||||||
EventHubEventDto event,
|
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(
|
return new RuntimeVehicleEvidenceAttachmentDecisionDto(
|
||||||
decision,
|
decision,
|
||||||
reason,
|
reason,
|
||||||
|
|
@ -198,20 +226,20 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
|
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
|
||||||
scopeClassifier.classify(event),
|
scopeClassifier.classify(event),
|
||||||
vehicleKeys(event),
|
vehicleKeys(event),
|
||||||
intervalIds
|
intervalIds == null ? List.of() : List.copyOf(intervalIds)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
|
private List<DriverWorkingTimeVehicleUsageInterval> matchingUsageIntervals(
|
||||||
EventHubEventDto vehicleEvent,
|
EventHubEventDto vehicleEvent,
|
||||||
List<ResolvedVehicleUsageInterval> usageIntervals,
|
List<DriverWorkingTimeVehicleUsageInterval> usageIntervals,
|
||||||
int paddingMinutes
|
int paddingMinutes
|
||||||
) {
|
) {
|
||||||
if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
|
if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
|
List<DriverWorkingTimeVehicleUsageInterval> result = new ArrayList<>();
|
||||||
for (ResolvedVehicleUsageInterval interval : usageIntervals) {
|
for (DriverWorkingTimeVehicleUsageInterval interval : usageIntervals) {
|
||||||
if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
|
if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
|
||||||
result.add(interval);
|
result.add(interval);
|
||||||
}
|
}
|
||||||
|
|
@ -219,16 +247,20 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean timeInside(OffsetDateTime occurredAt, ResolvedVehicleUsageInterval interval, int paddingMinutes) {
|
private boolean timeInside(
|
||||||
if (occurredAt == null || interval == null || interval.from() == null) {
|
OffsetDateTime occurredAt,
|
||||||
|
DriverWorkingTimeVehicleUsageInterval interval,
|
||||||
|
int paddingMinutes
|
||||||
|
) {
|
||||||
|
if (occurredAt == null || interval == null || interval.startedAt() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
OffsetDateTime from = interval.from().minusMinutes(paddingMinutes);
|
OffsetDateTime from = interval.startedAt().minusMinutes(paddingMinutes);
|
||||||
OffsetDateTime to = interval.to() == null ? OffsetDateTime.MAX : interval.to().plusMinutes(paddingMinutes);
|
OffsetDateTime to = interval.endedAt() == null ? OffsetDateTime.MAX : interval.endedAt().plusMinutes(paddingMinutes);
|
||||||
return !occurredAt.isBefore(from) && !occurredAt.isAfter(to);
|
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) {
|
if (event == null || interval == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +294,7 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
return Set.copyOf(result);
|
return Set.copyOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> vehicleKeys(ResolvedVehicleUsageInterval interval) {
|
private Set<String> vehicleKeys(DriverWorkingTimeVehicleUsageInterval interval) {
|
||||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||||
add(result, interval.vehicleKey());
|
add(result, interval.vehicleKey());
|
||||||
add(result, interval.registrationKey());
|
add(result, interval.registrationKey());
|
||||||
|
|
@ -275,6 +307,13 @@ public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||||
return Set.copyOf(result);
|
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) {
|
private void add(Set<String> keys, String value) {
|
||||||
if (value != null && !value.isBlank()) {
|
if (value != null && !value.isBlank()) {
|
||||||
keys.add(value.trim());
|
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(
|
private List<EventHubEventDto> deduplicateAndSort(
|
||||||
List<EventHubEventDto> directDriverEvents,
|
List<EventHubEventDto> directDriverEvents,
|
||||||
List<EventHubEventDto> vehicleEvidenceEvents
|
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.UnifiedRuntimeDerivedProjectionResultDto;
|
||||||
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
|
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
|
||||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
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.RuntimeSupportEvidenceNormalizationResult;
|
||||||
|
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceEvent;
|
||||||
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
|
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
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.DriverTimelineBuilder;
|
||||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
|
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
|
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
|
||||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
|
@ -63,7 +66,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
||||||
driverTimelineBuilder,
|
driverTimelineBuilder,
|
||||||
reusableProjectionBuilder,
|
reusableProjectionBuilder,
|
||||||
properties,
|
properties,
|
||||||
new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
|
new DriverWorkingTimeProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties),
|
||||||
supportEvidenceNormalizer
|
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.");
|
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),
|
runtimeSessionId(request),
|
||||||
driverKey,
|
driverKey,
|
||||||
timeline,
|
timeline.sourceKind(),
|
||||||
normalizedEvents,
|
timeline.loadedFrom(),
|
||||||
|
timeline.loadedTo(),
|
||||||
requestedFrom,
|
requestedFrom,
|
||||||
requestedTo,
|
requestedTo,
|
||||||
significantDrivingMinutes,
|
significantDrivingMinutes,
|
||||||
minimumRestPeriodMinutes,
|
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
|
notes
|
||||||
));
|
);
|
||||||
|
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(processingInput);
|
||||||
notes = projection.notes();
|
notes = projection.notes();
|
||||||
|
|
||||||
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
|
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
|
||||||
|
|
@ -185,6 +202,36 @@ public class UnifiedRuntimeDerivedProjectionService {
|
||||||
return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
|
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(
|
private String resolveDriverKey(
|
||||||
UnifiedRuntimeProcessingRequest request,
|
UnifiedRuntimeProcessingRequest request,
|
||||||
List<EventHubEventDto> events
|
List<EventHubEventDto> events
|
||||||
|
|
|
||||||
|
|
@ -2,765 +2,39 @@ package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
import at.procon.eventhub.config.EventHubProperties;
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
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.dto.TachographEsperDriverProcessingResultDto;
|
||||||
import at.procon.eventhub.tachographfilesession.model.*;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
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.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link DriverWorkingTimeProcessingCore}. This class remains as a
|
||||||
|
* compatibility adapter for existing tachograph file-session callers.
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Deprecated(forRemoval = false)
|
@Deprecated(forRemoval = false)
|
||||||
public class TachographEsperProcessingCore {
|
public class TachographEsperProcessingCore {
|
||||||
|
|
||||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
private final DriverWorkingTimeProcessingCore delegate;
|
||||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
|
||||||
private final EventHubProperties properties;
|
@Autowired
|
||||||
|
public TachographEsperProcessingCore(DriverWorkingTimeProcessingCore delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
public TachographEsperProcessingCore(
|
public TachographEsperProcessingCore(
|
||||||
DriverTimelineBuilder driverTimelineBuilder,
|
DriverTimelineBuilder driverTimelineBuilder,
|
||||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||||
EventHubProperties properties
|
EventHubProperties properties
|
||||||
) {
|
) {
|
||||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
this(new DriverWorkingTimeProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties));
|
||||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
|
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
|
||||||
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(processDriverWorkingTime(input));
|
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(delegate.process(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
|
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
|
||||||
Objects.requireNonNull(input, "input must not be null");
|
return delegate.process(input);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue