diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java new file mode 100644 index 0000000..a27dbd5 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographEsperDrivingDerivedProjectionBundle.java @@ -0,0 +1,12 @@ +package at.procon.eventhub.tachographfilesession.model; + +import java.util.List; + +public record TachographEsperDrivingDerivedProjectionBundle( + List drivingInterruptionIntervals, + List dailyWeeklyRestCandidateIntervals, + List drivingInterruptionVehicleChangeIntervals, + List vuCardAbsentIntervals, + List potentialHomeOvernightStayIntervals +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographTimelineEventBundle.java b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographTimelineEventBundle.java new file mode 100644 index 0000000..503f0dc --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/model/TachographTimelineEventBundle.java @@ -0,0 +1,35 @@ +package at.procon.eventhub.tachographfilesession.model; + +import at.procon.eventhub.dto.EventHubEventDto; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public record TachographTimelineEventBundle( + List activityEvents, + List vehicleUsageEvents, + List supportEvents +) { + public TachographTimelineEventBundle { + activityEvents = copy(activityEvents); + vehicleUsageEvents = copy(vehicleUsageEvents); + supportEvents = copy(supportEvents); + } + + public List allEvents() { + List result = new ArrayList<>(activityEvents.size() + vehicleUsageEvents.size() + supportEvents.size()); + result.addAll(activityEvents); + result.addAll(vehicleUsageEvents); + result.addAll(supportEvents); + result.sort(Comparator.comparing(EventHubEventDto::occurredAt) + .thenComparing(event -> event.eventDomain().name()) + .thenComparing(event -> event.eventType().name()) + .thenComparing(event -> event.lifecycle().name()) + .thenComparing(EventHubEventDto::externalSourceEventId)); + return List.copyOf(result); + } + + private static List copy(List events) { + return events == null ? List.of() : List.copyOf(events); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineEventBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineEventBuilder.java new file mode 100644 index 0000000..08b56f4 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineEventBuilder.java @@ -0,0 +1,37 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle; +import java.util.List; + +public interface DriverTimelineEventBuilder { + + TachographTimelineEventBundle buildEventBundle( + TachographFileSession session, + DriverExtractionSession driverSession + ); + + TachographTimelineEventBundle buildEventBundle( + TachographFileSession session, + DriverExtractionSession driverSession, + ResolvedDriverTimeline timeline + ); + + default List buildEvents( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + return buildEventBundle(session, driverSession).allEvents(); + } + + default List buildEvents( + TachographFileSession session, + DriverExtractionSession driverSession, + ResolvedDriverTimeline timeline + ) { + return buildEventBundle(session, driverSession, timeline).allEvents(); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java new file mode 100644 index 0000000..666d18a --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -0,0 +1,442 @@ +package at.procon.eventhub.tachographfilesession.service; + +import com.espertech.esper.common.client.EPCompiled; +import com.espertech.esper.common.client.EventBean; +import com.espertech.esper.common.client.configuration.Configuration; +import com.espertech.esper.compiler.client.CompilerArguments; +import com.espertech.esper.compiler.client.EPCompileException; +import com.espertech.esper.compiler.client.EPCompilerProvider; +import com.espertech.esper.runtime.client.EPDeployException; +import com.espertech.esper.runtime.client.EPDeployment; +import com.espertech.esper.runtime.client.EPRuntime; +import com.espertech.esper.runtime.client.EPRuntimeProvider; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +@Component +public class DriverTimelineReusableProjectionBuilder { + + private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); + private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE = + loadResource("esper/tachograph-driving-derived-projection-bundle.epl"); + + private final DriverTimelineBuilder driverTimelineBuilder; + + public DriverTimelineReusableProjectionBuilder(DriverTimelineBuilder driverTimelineBuilder) { + this.driverTimelineBuilder = driverTimelineBuilder; + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + TachographFileSession session, + DriverExtractionSession driverSession, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); + return buildEsperDrivingDerivedProjectionBundle( + session.sessionId(), + driverSession.driverKey(), + timeline, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + UUID sessionId, + String driverKey, + ResolvedDriverTimeline timeline, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if (timeline == null) { + return emptyBundle(); + } + return buildEsperDrivingDerivedProjectionBundle( + sessionId, + driverKey, + timeline.activityIntervals(), + timeline.vehicleUsageIntervals(), + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( + UUID sessionId, + String driverKey, + List activityIntervals, + List vehicleUsageIntervals, + int significantDrivingMinutes, + int minimumRestPeriodMinutes + ) { + if ((activityIntervals == null || activityIntervals.isEmpty()) + && (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty())) { + return emptyBundle(); + } + + List drivingInterruptionIntervals = new ArrayList<>(); + List dailyWeeklyRestCandidateIntervals = new ArrayList<>(); + List drivingInterruptionVehicleChangeIntervals = new ArrayList<>(); + List vuCardAbsentIntervals = new ArrayList<>(); + List potentialHomeOvernightStayIntervals = new ArrayList<>(); + + executeWithRuntime( + configuration -> { + configuration.getCommon().addEventType( + "TachographActivityIntervalInputEvent", + activityIntervalInputDefinition() + ); + configuration.getCommon().addEventType( + "TachographVehicleUsageIntervalInputEvent", + vehicleUsageIntervalInputDefinition() + ); + }, + renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes), + Map.of( + "drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals), + "dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals), + "drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals), + "vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals), + "potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals) + ), + runtime -> { + if (vehicleUsageIntervals != null) { + for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { + runtime.getEventService().sendEventMap( + toVehicleUsageIntervalInputMap(interval), + "TachographVehicleUsageIntervalInputEvent" + ); + } + } + if (activityIntervals != null) { + for (ResolvedActivityInterval interval : activityIntervals) { + runtime.getEventService().sendEventMap( + toActivityIntervalInputMap(sessionId, driverKey, interval), + "TachographActivityIntervalInputEvent" + ); + } + } + } + ); + + return new TachographEsperDrivingDerivedProjectionBundle( + sortDrivingInterruptionIntervals(drivingInterruptionIntervals), + sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals), + sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals), + sortVuCardAbsentIntervals(vuCardAbsentIntervals), + sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals) + ); + } + + private TachographEsperDrivingDerivedProjectionBundle emptyBundle() { + return new TachographEsperDrivingDerivedProjectionBundle( + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + ); + } + + private void executeWithRuntime( + Consumer configurationSetup, + String epl, + Map> listeners, + Consumer sender + ) { + EPRuntime runtime = null; + try { + Configuration configuration = new Configuration(); + configurationSetup.accept(configuration); + String runtimeUri = "eventhub-tachograph-reusable-projection-" + RUNTIME_COUNTER.incrementAndGet(); + runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration); + + CompilerArguments arguments = new CompilerArguments(configuration); + EPCompiled compiled = EPCompilerProvider.getCompiler().compile(epl, arguments); + EPDeployment deployment = runtime.getDeploymentService().deploy(compiled); + for (Map.Entry> entry : listeners.entrySet()) { + runtime.getDeploymentService() + .getStatement(deployment.getDeploymentId(), entry.getKey()) + .addListener((newData, oldData, statement, rt) -> entry.getValue().accept(newData)); + } + + sender.accept(runtime); + } catch (EPCompileException | EPDeployException e) { + throw new IllegalStateException("Cannot compile/deploy reusable tachograph projection EPL bundle", e); + } finally { + if (runtime != null) { + runtime.destroy(); + } + } + } + + private Map activityIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("activityType", String.class); + definition.put("cardSlot", String.class); + definition.put("cardStatus", String.class); + definition.put("drivingStatus", String.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("firstSourceIntervalId", String.class); + definition.put("lastSourceIntervalId", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", long.class); + definition.put("durationSeconds", long.class); + definition.put("sourceIntervalIds", java.util.List.class); + definition.put("synthetic", boolean.class); + definition.put("clippedToRequestedPeriod", boolean.class); + definition.put("level", String.class); + return definition; + } + + private Map vehicleUsageIntervalInputDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("firstSourceIntervalId", String.class); + definition.put("lastSourceIntervalId", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", Long.class); + definition.put("durationSeconds", long.class); + definition.put("odometerBeginKm", Long.class); + definition.put("odometerEndKm", Long.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("sourceIntervalIds", java.util.List.class); + return definition; + } + + private Map toActivityIntervalInputMap( + UUID sessionId, + String driverKey, + ResolvedActivityInterval interval + ) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", sessionId); + event.put("driverKey", driverKey); + event.put("intervalId", interval.intervalId()); + event.put("activityType", interval.activityType()); + event.put("cardSlot", interval.slot()); + event.put("cardStatus", interval.cardStatus()); + event.put("drivingStatus", interval.drivingStatus()); + event.put("registrationKey", interval.registrationKey()); + event.put("vehicleKey", interval.vehicleKey()); + event.put("sourceKind", interval.sourceKind()); + event.put("firstSourceIntervalId", firstSourceIntervalId(interval)); + event.put("lastSourceIntervalId", lastSourceIntervalId(interval)); + event.put("startedAt", interval.from()); + event.put("endedAt", interval.to()); + event.put("startedAtEpochSecond", interval.from().toEpochSecond()); + event.put("endedAtEpochSecond", interval.to().toEpochSecond()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("sourceIntervalIds", interval.sourceIntervalIds()); + event.put("synthetic", interval.synthetic()); + event.put("clippedToRequestedPeriod", interval.clippedToRequestedPeriod()); + event.put("level", interval.level()); + return event; + } + + private Map toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) { + Map event = new LinkedHashMap<>(); + event.put("sessionId", interval.sessionId()); + event.put("driverKey", interval.driverKey()); + event.put("intervalId", interval.intervalId()); + event.put("firstSourceIntervalId", firstSourceIntervalId(interval)); + event.put("lastSourceIntervalId", lastSourceIntervalId(interval)); + event.put("startedAt", interval.from()); + event.put("endedAt", interval.to()); + event.put("startedAtEpochSecond", interval.from().toEpochSecond()); + event.put("endedAtEpochSecond", interval.to() == null ? null : interval.to().toEpochSecond()); + event.put("durationSeconds", interval.durationSeconds()); + event.put("odometerBeginKm", interval.odometerBeginKm()); + event.put("odometerEndKm", interval.odometerEndKm()); + event.put("registrationKey", interval.registrationKey()); + event.put("vehicleKey", interval.vehicleKey()); + event.put("sourceKind", interval.sourceKind()); + event.put("sourceIntervalIds", interval.sourceIntervalIds()); + return event; + } + + private String firstSourceIntervalId(ResolvedActivityInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); + } + + private String lastSourceIntervalId(ResolvedActivityInterval interval) { + return interval.sourceIntervalIds().isEmpty() + ? interval.intervalId() + : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1); + } + + private String firstSourceIntervalId(ResolvedVehicleUsageInterval interval) { + return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); + } + + private String lastSourceIntervalId(ResolvedVehicleUsageInterval interval) { + return interval.sourceIntervalIds().isEmpty() + ? interval.intervalId() + : interval.sourceIntervalIds().get(interval.sourceIntervalIds().size() - 1); + } + + private void collectDrivingInterruptionIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperDrivingInterruptionIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), + (Long) event.get("durationSeconds"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + + private void collectVuCardAbsentIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperVuCardAbsentIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), + (Long) event.get("durationSeconds"), + (String) event.get("previousUsageIntervalId"), + (String) event.get("nextUsageIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + + private void collectPotentialHomeOvernightStayIntervalEvents( + EventBean[] newData, + List target + ) { + if (newData == null) { + return; + } + for (EventBean event : newData) { + long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); + long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); + target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent( + (UUID) event.get("sessionId"), + (String) event.get("driverKey"), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(startedAtEpochSecond), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochSecond(endedAtEpochSecond), ZoneOffset.UTC), + (Long) event.get("durationSeconds"), + (Long) event.get("unknownDurationSeconds"), + (Double) event.get("unknownCoveragePercent"), + (String) event.get("previousDrivingSourceIntervalId"), + (String) event.get("nextDrivingSourceIntervalId"), + (String) event.get("previousRegistrationKey"), + (String) event.get("nextRegistrationKey"), + (String) event.get("previousVehicleKey"), + (String) event.get("nextVehicleKey") + )); + } + } + + private List sortDrivingInterruptionIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt) + .thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt)) + .toList(); + } + + private List sortVuCardAbsentIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt) + .thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt)) + .toList(); + } + + private List sortPotentialHomeOvernightStayIntervals( + List intervals + ) { + return intervals.stream() + .sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt) + .thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt)) + .toList(); + } + + private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) { + return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE + .replace( + "${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}", + Long.toString(Math.max(1, significantDrivingMinutes) * 60L) + ) + .replace( + "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}", + Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L) + ); + } + + private static String loadResource(String path) { + try { + ClassPathResource resource = new ClassPathResource(path); + return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Cannot load EPL resource: " + path, e); + } + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java new file mode 100644 index 0000000..a8c3b01 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilder.java @@ -0,0 +1,615 @@ +package at.procon.eventhub.tachographfilesession.service; + +import at.procon.eventhub.dto.CardSlot; +import at.procon.eventhub.dto.CardStatus; +import at.procon.eventhub.dto.DriverCardRefDto; +import at.procon.eventhub.dto.DriverRefDto; +import at.procon.eventhub.dto.DrivingStatus; +import at.procon.eventhub.dto.EventDetailsDto; +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventHubPackageRequest; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventSourceDto; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.dto.GeoPointDto; +import at.procon.eventhub.dto.ImportScopeDto; +import at.procon.eventhub.dto.SourcePackageRefDto; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.dto.VehicleRegistrationRefDto; +import at.procon.eventhub.service.EventDetailsFactory; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriver; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard; +import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration; +import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.springframework.stereotype.Component; + +@Component +public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineEventBuilder { + + private final DriverTimelineBuilder driverTimelineBuilder; + private final DriverKeyFactory driverKeyFactory; + private final VehicleKeyFactory vehicleKeyFactory; + private final EventDetailsFactory detailsFactory; + + public IntervalBackedDriverTimelineEventBuilder( + DriverTimelineBuilder driverTimelineBuilder, + DriverKeyFactory driverKeyFactory, + VehicleKeyFactory vehicleKeyFactory, + EventDetailsFactory detailsFactory + ) { + this.driverTimelineBuilder = driverTimelineBuilder; + this.driverKeyFactory = driverKeyFactory; + this.vehicleKeyFactory = vehicleKeyFactory; + this.detailsFactory = detailsFactory; + } + + @Override + public TachographTimelineEventBundle buildEventBundle( + TachographFileSession session, + DriverExtractionSession driverSession + ) { + ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession); + return buildEventBundle(session, driverSession, timeline); + } + + @Override + public TachographTimelineEventBundle buildEventBundle( + TachographFileSession session, + DriverExtractionSession driverSession, + ResolvedDriverTimeline timeline + ) { + if (session == null || driverSession == null || timeline == null) { + return new TachographTimelineEventBundle(List.of(), List.of(), List.of()); + } + + Map registrationsByKey = new LinkedHashMap<>(); + for (ExtractedVehicleRegistration registration : driverSession.vehicleRegistrations()) { + registrationsByKey.put(registration.registrationKey(), registration); + } + Map vehiclesByKey = new LinkedHashMap<>(); + for (ExtractedVehicle vehicle : driverSession.vehicles()) { + vehiclesByKey.put(vehicle.vehicleKey(), vehicle); + } + + DriverRefDto driverRef = driverRef(driverSession); + EventSourceDto eventSource = eventSource(session, timeline); + SourcePackageRefDto sourcePackageRef = sourcePackageRef(session, timeline); + + List activityEvents = buildActivityEvents( + session, + timeline.activityIntervals(), + driverRef, + registrationsByKey, + vehiclesByKey, + eventSource, + sourcePackageRef + ); + List vehicleUsageEvents = buildVehicleUsageEvents( + session, + timeline.vehicleUsageIntervals(), + driverRef, + registrationsByKey, + vehiclesByKey, + eventSource, + sourcePackageRef + ); + List supportEvents = buildSupportEvents( + session, + timeline.supportEvents(), + driverRef, + registrationsByKey, + vehiclesByKey, + eventSource, + sourcePackageRef + ); + return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents); + } + + private List buildActivityEvents( + TachographFileSession session, + List intervals, + DriverRefDto driverRef, + Map registrationsByKey, + Map vehiclesByKey, + EventSourceDto eventSource, + SourcePackageRefDto sourcePackageRef + ) { + if (intervals == null || intervals.isEmpty()) { + return List.of(); + } + List events = new ArrayList<>(intervals.size() * 2); + for (ResolvedActivityInterval interval : intervals) { + VehicleRefDto vehicleRef = vehicleRef(interval.registrationKey(), interval.vehicleKey(), registrationsByKey, vehiclesByKey); + EventType eventType = activityEventType(interval.activityType()); + EventDetailsDto details = detailsFactory.driverActivity( + cardSlot(interval.slot()), + cardStatus(interval.cardStatus()), + drivingStatus(interval.drivingStatus()) + ); + Map raw = new LinkedHashMap<>(); + raw.put("intervalId", interval.intervalId()); + raw.put("sourceRowId", interval.intervalId()); + raw.put("sourceRowIds", interval.sourceIntervalIds()); + raw.put("startedAt", timeText(interval.from())); + raw.put("endedAt", timeText(interval.to())); + raw.put("durationSeconds", interval.durationSeconds()); + raw.put("sourceKind", interval.sourceKind()); + raw.put("registrationKey", interval.registrationKey()); + raw.put("vehicleKey", interval.vehicleKey()); + raw.put("synthetic", interval.synthetic()); + raw.put("clippedToRequestedPeriod", interval.clippedToRequestedPeriod()); + raw.put("level", interval.level()); + + events.add(event( + session, + interval.from(), + EventDomain.DRIVER_ACTIVITY, + eventType, + EventLifecycle.START, + eventSource, + driverRef, + vehicleRef, + null, + null, + details, + sourcePackageRef, + raw, + isManualEntry(interval.cardStatus(), interval.drivingStatus()), + "ACTIVITY", + interval.intervalId() + )); + events.add(event( + session, + interval.to(), + EventDomain.DRIVER_ACTIVITY, + eventType, + EventLifecycle.END, + eventSource, + driverRef, + vehicleRef, + null, + null, + details, + sourcePackageRef, + raw, + isManualEntry(interval.cardStatus(), interval.drivingStatus()), + "ACTIVITY", + interval.intervalId() + )); + } + return List.copyOf(events); + } + + private List buildVehicleUsageEvents( + TachographFileSession session, + List intervals, + DriverRefDto driverRef, + Map registrationsByKey, + Map vehiclesByKey, + EventSourceDto eventSource, + SourcePackageRefDto sourcePackageRef + ) { + if (intervals == null || intervals.isEmpty()) { + return List.of(); + } + List events = new ArrayList<>(intervals.size() * 2); + for (ResolvedVehicleUsageInterval interval : intervals) { + VehicleRefDto vehicleRef = vehicleRef(interval.registrationKey(), interval.vehicleKey(), registrationsByKey, vehiclesByKey); + EventDetailsDto insertDetails = detailsFactory.driverCard( + null, + CardStatus.INSERTED, + driverRef == null ? null : driverRef.driverCard() + ); + EventDetailsDto withdrawDetails = detailsFactory.driverCard( + null, + CardStatus.NOT_INSERTED, + driverRef == null ? null : driverRef.driverCard() + ); + Map raw = new LinkedHashMap<>(); + raw.put("intervalId", interval.intervalId()); + raw.put("sourceRowId", interval.intervalId()); + raw.put("sourceRowIds", interval.sourceIntervalIds()); + raw.put("startedAt", timeText(interval.from())); + raw.put("endedAt", timeText(interval.to())); + raw.put("durationSeconds", interval.durationSeconds()); + raw.put("registrationKey", interval.registrationKey()); + raw.put("vehicleKey", interval.vehicleKey()); + raw.put("sourceKind", interval.sourceKind()); + raw.put("odometerBeginKm", interval.odometerBeginKm()); + raw.put("odometerEndKm", interval.odometerEndKm()); + + events.add(event( + session, + interval.from(), + EventDomain.DRIVER_CARD, + EventType.CARD_INSERTED, + EventLifecycle.INSERT, + eventSource, + driverRef, + vehicleRef, + odometerMeters(interval.odometerBeginKm()), + null, + insertDetails, + sourcePackageRef, + raw, + false, + "VEHICLE_USAGE", + interval.intervalId() + )); + if (interval.to() != null) { + events.add(event( + session, + interval.to(), + EventDomain.DRIVER_CARD, + EventType.CARD_WITHDRAWN, + EventLifecycle.WITHDRAW, + eventSource, + driverRef, + vehicleRef, + odometerMeters(interval.odometerEndKm()), + null, + withdrawDetails, + sourcePackageRef, + raw, + false, + "VEHICLE_USAGE", + interval.intervalId() + )); + } + } + return List.copyOf(events); + } + + private List buildSupportEvents( + TachographFileSession session, + List supportEvents, + DriverRefDto driverRef, + Map registrationsByKey, + Map vehiclesByKey, + EventSourceDto eventSource, + SourcePackageRefDto sourcePackageRef + ) { + if (supportEvents == null || supportEvents.isEmpty()) { + return List.of(); + } + List events = new ArrayList<>(supportEvents.size()); + for (ExtractedSupportEvent supportEvent : supportEvents) { + EventDomain eventDomain = supportEventDomain(supportEvent.eventDomain()); + EventType eventType = supportEventType(eventDomain, supportEvent.eventType(), supportEvent.code()); + EventLifecycle lifecycle = supportEventLifecycle(eventDomain, supportEvent.eventType()); + boolean manualEntry = isManualPlaceEvent(supportEvent.eventType()); + VehicleRefDto vehicleRef = vehicleRef( + supportEvent.registrationKey(), + supportEvent.vehicleKey(), + registrationsByKey, + vehiclesByKey + ); + EventDetailsDto details = supportDetails(eventDomain, supportEvent); + Map raw = new LinkedHashMap<>(); + raw.put("sourceRowId", supportEvent.eventId()); + raw.put("supportEventId", supportEvent.eventId()); + raw.put("supportEventType", supportEvent.eventType()); + raw.put("slot", supportEvent.slot()); + raw.put("registrationKey", supportEvent.registrationKey()); + raw.put("vehicleKey", supportEvent.vehicleKey()); + raw.put("country", supportEvent.country()); + raw.put("region", supportEvent.region()); + raw.put("authenticationStatus", supportEvent.authenticationStatus()); + raw.put("odometerKm", supportEvent.odometerKm()); + raw.put("code", supportEvent.code()); + raw.put("rawRecordPath", supportEvent.rawRecordPath()); + + events.add(event( + session, + supportEvent.occurredAt(), + eventDomain, + eventType, + lifecycle, + eventSource, + driverRef, + vehicleRef, + odometerMeters(supportEvent.odometerKm()), + position(supportEvent.latitude(), supportEvent.longitude()), + details, + sourcePackageRef, + raw, + manualEntry, + "SUPPORT", + supportEvent.eventId() + )); + } + return List.copyOf(events); + } + + private EventHubEventDto event( + TachographFileSession session, + OffsetDateTime occurredAt, + EventDomain eventDomain, + EventType eventType, + EventLifecycle lifecycle, + EventSourceDto eventSource, + DriverRefDto driverRef, + VehicleRefDto vehicleRef, + Long odometerM, + GeoPointDto position, + EventDetailsDto details, + SourcePackageRefDto sourcePackageRef, + Map rawPayload, + boolean manualEntry, + String group, + String sourceId + ) { + return new EventHubEventDto( + UUID.randomUUID(), + externalSourceEventId(session.sessionId(), group, sourceId, lifecycle, occurredAt), + driverRef, + vehicleRef, + occurredAt, + null, + OffsetDateTime.now(), + eventDomain, + eventType, + lifecycle, + odometerM, + position, + details, + sourcePackageRef, + detailsFactory.payloadFromMap(Map.of("raw", rawPayload)), + manualEntry, + packageInfo(session, eventSource, eventDomain, occurredAt) + ); + } + + private DriverRefDto driverRef(DriverExtractionSession driverSession) { + ExtractedDriver driver = driverSession.driver(); + ExtractedDriverCard card = driverSession.driverCard(); + DriverCardRefDto driverCardRef = card == null || card.cardNumber() == null + ? driverCardFromKey(driverSession.driverKey()) + : new DriverCardRefDto(card.cardNation(), card.cardNumber()); + String sourceDriverId = driver == null || driver.sourceDriverId() == null + ? driverKeyFactory.createSourceDriverId(driverSession.driverKey()) + : driver.sourceDriverId(); + return new DriverRefDto(sourceDriverId, driverCardRef); + } + + private DriverCardRefDto driverCardFromKey(String driverKey) { + if (driverKey == null || driverKey.isBlank()) { + return null; + } + int separator = driverKey.indexOf(':'); + if (separator < 0) { + return new DriverCardRefDto(null, driverKey); + } + return new DriverCardRefDto(driverKey.substring(0, separator), driverKey.substring(separator + 1)); + } + + private VehicleRefDto vehicleRef( + String registrationKey, + String vehicleKey, + Map registrationsByKey, + Map vehiclesByKey + ) { + ExtractedVehicleRegistration registration = registrationKey == null ? null : registrationsByKey.get(registrationKey); + ExtractedVehicle vehicle = vehicleKey == null ? null : vehiclesByKey.get(vehicleKey); + VehicleRegistrationRefDto registrationRef = registration != null + ? new VehicleRegistrationRefDto(registration.registrationNation(), registration.registrationNumber()) + : registrationFromKey(registrationKey); + VehicleRefDto vehicleRef = new VehicleRefDto( + vehicle != null ? vehicle.sourceVehicleId() : vehicleKeyFactory.createSourceVehicleId(vehicleKey), + vehicle != null ? vehicle.vin() : vehicleKey, + registration != null ? registration.sourceVehicleRegistrationId() : vehicleKeyFactory.createSourceVehicleRegistrationId(registrationKey), + registrationRef + ); + return vehicleRef.hasAnyReference() ? vehicleRef : null; + } + + private VehicleRegistrationRefDto registrationFromKey(String registrationKey) { + if (registrationKey == null || registrationKey.isBlank()) { + return null; + } + int separator = registrationKey.indexOf(':'); + if (separator < 0) { + return new VehicleRegistrationRefDto(null, registrationKey); + } + return new VehicleRegistrationRefDto(registrationKey.substring(0, separator), registrationKey.substring(separator + 1)); + } + + private EventSourceDto eventSource(TachographFileSession session, ResolvedDriverTimeline timeline) { + String sourceKind = timeline.sourceKind() == null || timeline.sourceKind().isBlank() + ? (session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT") + : timeline.sourceKind(); + return new EventSourceDto( + "TACHOGRAPH", + sourceKind, + "TACHOGRAPH_" + sourceKind, + session.metadata().sourceInstanceKey(), + null, + null + ); + } + + private SourcePackageRefDto sourcePackageRef(TachographFileSession session, ResolvedDriverTimeline timeline) { + return new SourcePackageRefDto( + "TACHOGRAPH_FILE_SESSION", + session.sessionId().toString(), + session.metadata().uploadedFileSha256(), + timeline.loadedFrom(), + timeline.loadedTo(), + session.createdAt().atOffset(ZoneOffset.UTC) + ); + } + + private EventHubPackageRequest packageInfo( + TachographFileSession session, + EventSourceDto eventSource, + EventDomain eventDomain, + OffsetDateTime occurredAt + ) { + LocalDate businessDate = occurredAt == null ? null : occurredAt.toLocalDate(); + return new EventHubPackageRequest( + tenantOrDefault(session.metadata().tenantKey()), + eventSource, + null, + ImportScopeDto.tenantAll(null, null), + eventDomain.name(), + businessDate, + "TACHOGRAPH_FILE_SESSION:" + session.sessionId() + ":" + eventDomain.name() + ":" + businessDate + ); + } + + private EventType activityEventType(String activityType) { + if (activityType == null) { + return EventType.UNKNOWN_ACTIVITY; + } + return switch (activityType) { + case "DRIVE" -> EventType.DRIVE; + case "WORK" -> EventType.WORK; + case "AVAILABILITY" -> EventType.AVAILABILITY; + case "BREAK_REST" -> EventType.BREAK_REST; + default -> EventType.UNKNOWN_ACTIVITY; + }; + } + + private CardSlot cardSlot(String value) { + return parseEnum(CardSlot.class, value, null); + } + + private CardStatus cardStatus(String value) { + return parseEnum(CardStatus.class, value, null); + } + + private DrivingStatus drivingStatus(String value) { + return parseEnum(DrivingStatus.class, value, DrivingStatus.UNKNOWN); + } + + private boolean isManualEntry(String cardStatus, String drivingStatus) { + return Objects.equals("NOT_INSERTED", normalizeToken(cardStatus)) + && Objects.equals("KNOWN", normalizeToken(drivingStatus)); + } + + private EventDomain supportEventDomain(String value) { + return switch (normalizeToken(value)) { + case "PLACE" -> EventDomain.PLACE; + case "POSITION" -> EventDomain.POSITION; + case "SPECIFIC_CONDITION" -> EventDomain.SPECIFIC_CONDITION; + default -> EventDomain.TELEMATICS_DATA; + }; + } + + private EventType supportEventType(EventDomain eventDomain, String eventType, String code) { + return switch (eventDomain) { + case PLACE -> EventType.WORKING_DAY_PLACE_RECORDED; + case POSITION -> EventType.POSITION_RECORDED; + case SPECIFIC_CONDITION -> { + String normalizedCode = normalizeToken(code); + if (Objects.equals("FERRY_TRAIN", normalizedCode) || Objects.equals("FERRY_OR_TRAIN", normalizedCode)) { + yield EventType.FERRY_TRAIN; + } + if (Objects.equals("OUT_OF_SCOPE", normalizedCode) || Objects.equals("OUT", normalizedCode)) { + yield EventType.OUT; + } + yield EventType.UNKNOWN_EVENT; + } + default -> parseEnum(EventType.class, eventType, EventType.UNKNOWN_EVENT); + }; + } + + private EventLifecycle supportEventLifecycle(EventDomain eventDomain, String eventType) { + if (eventDomain == EventDomain.PLACE) { + String normalized = normalizeToken(eventType); + if (normalized != null && normalized.startsWith("BEGIN")) { + return EventLifecycle.BEGIN; + } + if (normalized != null && normalized.startsWith("END")) { + return EventLifecycle.END; + } + } + return EventLifecycle.SNAPSHOT; + } + + private boolean isManualPlaceEvent(String eventType) { + String normalized = normalizeToken(eventType); + return normalized != null && normalized.contains("MANUAL"); + } + + private EventDetailsDto supportDetails(EventDomain eventDomain, ExtractedSupportEvent supportEvent) { + return switch (eventDomain) { + case PLACE -> detailsFactory.place(supportEvent.country(), supportEvent.region()); + case POSITION -> detailsFactory.position(supportEvent.eventType()); + case SPECIFIC_CONDITION -> detailsFactory.specificCondition(); + default -> new EventDetailsDto("TACHOGRAPH_SUPPORT", detailsFactory.payloadFromMap(Map.of())); + }; + } + + private GeoPointDto position(BigDecimal latitude, BigDecimal longitude) { + return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude); + } + + private Long odometerMeters(Long kilometers) { + return kilometers == null ? null : kilometers * 1_000L; + } + + private String timeText(OffsetDateTime value) { + return value == null ? null : value.toString(); + } + + private String externalSourceEventId( + UUID sessionId, + String group, + String sourceId, + EventLifecycle lifecycle, + OffsetDateTime occurredAt + ) { + return "TACHOGRAPH_FILE_SESSION:" + + sessionId + + ":" + + group + + ":" + + sourceId + + ":" + + lifecycle.name() + + ":" + + occurredAt; + } + + private String tenantOrDefault(String value) { + return value == null || value.isBlank() ? "default" : value.trim(); + } + + private String normalizeToken(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim().toUpperCase().replace('-', '_').replace(' ', '_'); + } + + private > T parseEnum(Class type, String value, T fallback) { + String normalized = normalizeToken(value); + if (normalized == null) { + return fallback; + } + try { + return Enum.valueOf(type, normalized); + } catch (IllegalArgumentException ignored) { + return fallback; + } + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java index 8dc23d2..6718cb4 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingService.java @@ -13,6 +13,7 @@ import at.procon.eventhub.tachographfilesession.model.ProcessedShiftDrivingEvalu import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; @@ -33,15 +34,18 @@ public class TachographFileSessionProcessingService { private final TachographFileSessionRepository repository; private final DriverTimelineBuilder driverTimelineBuilder; + private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; private final EventHubProperties properties; public TachographFileSessionProcessingService( TachographFileSessionRepository repository, DriverTimelineBuilder driverTimelineBuilder, + DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventHubProperties properties ) { this.repository = repository; this.driverTimelineBuilder = driverTimelineBuilder; + this.reusableProjectionBuilder = reusableProjectionBuilder; this.properties = properties; } @@ -161,13 +165,16 @@ public class TachographFileSessionProcessingService { requestedFrom, requestedTo ); - List rawDrivingInterruptionIntervals = - driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents( + TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle = + reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle( sessionId, driverKey, timeline, - significantDrivingMinutes + significantDrivingMinutes, + minimumRestPeriodMinutes ); + List rawDrivingInterruptionIntervals = + derivedProjectionBundle.drivingInterruptionIntervals(); List drivingInterruptionIntervals = clipEsperDrivingInterruptionIntervalEvents( rawDrivingInterruptionIntervals, @@ -175,10 +182,7 @@ public class TachographFileSessionProcessingService { requestedTo ); List rawDailyWeeklyRestCandidateIntervals = - driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents( - rawDrivingInterruptionIntervals, - minimumRestPeriodMinutes - ); + derivedProjectionBundle.dailyWeeklyRestCandidateIntervals(); List dailyWeeklyRestCandidateIntervals = clipEsperDrivingInterruptionIntervalEvents( rawDailyWeeklyRestCandidateIntervals, @@ -186,9 +190,7 @@ public class TachographFileSessionProcessingService { requestedTo ); List rawDrivingInterruptionVehicleChangeIntervals = - driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents( - rawDailyWeeklyRestCandidateIntervals - ); + derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals(); List drivingInterruptionVehicleChangeIntervals = clipEsperDrivingInterruptionIntervalEvents( rawDrivingInterruptionVehicleChangeIntervals, @@ -196,13 +198,10 @@ public class TachographFileSessionProcessingService { requestedTo ); List rawVuCardAbsentIntervals = - driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline); + derivedProjectionBundle.vuCardAbsentIntervals(); List potentialHomeOvernightStayIntervals = clipEsperPotentialHomeOvernightStayIntervalEvents( - driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents( - rawDrivingInterruptionVehicleChangeIntervals, - rawVuCardAbsentIntervals - ), + derivedProjectionBundle.potentialHomeOvernightStayIntervals(), rawVuCardAbsentIntervals, requestedFrom, requestedTo diff --git a/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl new file mode 100644 index 0000000..15473e2 --- /dev/null +++ b/src/main/resources/esper/tachograph-driving-derived-projection-bundle.epl @@ -0,0 +1,253 @@ +create schema SignificantDrivingInterval( + sessionId java.util.UUID, + driverKey string, + firstSourceIntervalId string, + lastSourceIntervalId string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + registrationKey string, + vehicleKey string +); + +create schema DrivingInterruptionInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DrivingInterruptionVehicleChangeInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent; + +create schema VuCardAbsentInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousUsageIntervalId string, + nextUsageIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema PotentialHomeOvernightStayInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + unknownDurationSeconds long, + unknownCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +insert into SignificantDrivingInterval +select + sessionId, + driverKey, + firstSourceIntervalId, + lastSourceIntervalId, + startedAtEpochSecond, + endedAtEpochSecond, + durationSeconds, + registrationKey, + vehicleKey +from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}); + +create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval; + +on SignificantDrivingInterval as next +insert into DrivingInterruptionInterval +select + priorInterval.sessionId as sessionId, + priorInterval.driverKey as driverKey, + priorInterval.endedAtEpochSecond as startedAtEpochSecond, + next.startedAtEpochSecond as endedAtEpochSecond, + next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds, + priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId, + next.firstSourceIntervalId as nextDrivingSourceIntervalId, + priorInterval.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, + priorInterval.vehicleKey as previousVehicleKey, + next.vehicleKey as nextVehicleKey +from PreviousSignificantDrivingInterval as priorInterval +where priorInterval.driverKey = next.driverKey + and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond; + +@Priority(20) +on SignificantDrivingInterval +delete from PreviousSignificantDrivingInterval; + +@Priority(10) +on SignificantDrivingInterval as current +insert into PreviousSignificantDrivingInterval +select *; + +insert into DailyWeeklyRestCandidateInterval +select * +from DrivingInterruptionInterval(durationSeconds > ${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}); + +insert into DrivingInterruptionVehicleChangeInterval +select * +from DailyWeeklyRestCandidateInterval( + previousRegistrationKey is not null, + nextRegistrationKey is not null, + previousRegistrationKey != nextRegistrationKey +); + +context PerDriver +create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; + +@Priority(30) +context PerDriver +on TachographVehicleUsageIntervalInputEvent as next +insert into VuCardAbsentInterval +select + priorInterval.sessionId as sessionId, + priorInterval.driverKey as driverKey, + priorInterval.endedAtEpochSecond + 1L as startedAtEpochSecond, + next.startedAtEpochSecond as endedAtEpochSecond, + next.startedAtEpochSecond - (priorInterval.endedAtEpochSecond + 1L) as durationSeconds, + priorInterval.lastSourceIntervalId as previousUsageIntervalId, + next.firstSourceIntervalId as nextUsageIntervalId, + priorInterval.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, + priorInterval.vehicleKey as previousVehicleKey, + next.vehicleKey as nextVehicleKey +from PreviousVehicleUsageInterval as priorInterval +where priorInterval.endedAt is not null + and next.startedAt is not null + and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond + 1L; + +@Priority(20) +context PerDriver +on TachographVehicleUsageIntervalInputEvent +delete from PreviousVehicleUsageInterval; + +@Priority(10) +context PerDriver +on TachographVehicleUsageIntervalInputEvent as current +insert into PreviousVehicleUsageInterval +select *; + +insert into PotentialHomeOvernightStayInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + sum( + case + when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.durationSeconds + when u.startedAtEpochSecond <= c.startedAtEpochSecond + then u.endedAtEpochSecond - c.startedAtEpochSecond + when u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end + ) as unknownDurationSeconds, + (sum( + case + when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.durationSeconds + when u.startedAtEpochSecond <= c.startedAtEpochSecond + then u.endedAtEpochSecond - c.startedAtEpochSecond + when u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end + ) * 100.0d) / c.durationSeconds as unknownCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey +from DrivingInterruptionVehicleChangeInterval as c unidirectional, + VuCardAbsentInterval#keepall as u +where u.driverKey = c.driverKey + and u.startedAtEpochSecond < c.endedAtEpochSecond + and u.endedAtEpochSecond > c.startedAtEpochSecond +group by + c.sessionId, + c.driverKey, + c.startedAtEpochSecond, + c.endedAtEpochSecond, + c.durationSeconds, + c.previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId, + c.previousRegistrationKey, + c.nextRegistrationKey, + c.previousVehicleKey, + c.nextVehicleKey +having sum( + case + when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.durationSeconds + when u.startedAtEpochSecond <= c.startedAtEpochSecond + then u.endedAtEpochSecond - c.startedAtEpochSecond + when u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end +) * 100L >= c.durationSeconds * 95L; + +@name('drivingInterruptionIntervals') +select * from DrivingInterruptionInterval; + +@name('dailyWeeklyRestCandidateIntervals') +select * from DailyWeeklyRestCandidateInterval; + +@name('drivingInterruptionVehicleChangeIntervals') +select * from DrivingInterruptionVehicleChangeInterval; + +@name('vuCardAbsentIntervals') +select * from VuCardAbsentInterval; + +@name('potentialHomeOvernightStayIntervals') +select * from PotentialHomeOvernightStayInterval; diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java new file mode 100644 index 0000000..e3f6ffd --- /dev/null +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilderTest.java @@ -0,0 +1,129 @@ +package at.procon.eventhub.tachographfilesession.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractionStats; +import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class DriverTimelineReusableProjectionBuilderTest { + + private final DriverTimelineBuilder legacyBuilder = new DriverTimelineBuilder(); + private final DriverTimelineReusableProjectionBuilder reusableBuilder = + new DriverTimelineReusableProjectionBuilder(legacyBuilder); + + @Test + void matchesLegacyDrivingDerivedProjectionChain() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + null, + null, + List.of(), + List.of(), + List.of( + new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + ), + new ExtractedCardVehicleUsageInterval( + "CVU-2", + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + OffsetDateTime.parse("2026-05-02T02:00:00Z"), + 201L, + 260L, + "12:REG-2", + "VIN-2", + "vu-2" + ) + ), + List.of( + new ExtractedCardActivityInterval( + "ACT-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "a" + ), + new ExtractedCardActivityInterval( + "ACT-2", + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:30:00Z"), + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-2", + "VIN-2", + "b" + ) + ), + List.of(), + List.of() + ); + TachographFileSession session = new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null), + Map.of(driver.driverKey(), driver), + new ExtractionStats(1, 2, 2, 1, 1, 0), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + ResolvedDriverTimeline timeline = legacyBuilder.build(session, driver); + List legacyDrivingInterruptions = + legacyBuilder.buildEsperDrivingInterruptionIntervalEvents(session.sessionId(), driver.driverKey(), timeline, 3); + List legacyDailyWeeklyRestCandidates = + legacyBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(legacyDrivingInterruptions, 720); + List legacyDrivingInterruptionVehicleChanges = + legacyBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(legacyDailyWeeklyRestCandidates); + List legacyVuCardAbsentIntervals = + legacyBuilder.buildEsperVuCardAbsentIntervalEvents(timeline); + List legacyPotentialHomeOvernightStays = + legacyBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents( + legacyDrivingInterruptionVehicleChanges, + legacyVuCardAbsentIntervals + ); + + TachographEsperDrivingDerivedProjectionBundle reusableBundle = + reusableBuilder.buildEsperDrivingDerivedProjectionBundle( + session.sessionId(), + driver.driverKey(), + timeline, + 3, + 720 + ); + + assertThat(reusableBundle.drivingInterruptionIntervals()).containsExactlyElementsOf(legacyDrivingInterruptions); + assertThat(reusableBundle.dailyWeeklyRestCandidateIntervals()).containsExactlyElementsOf(legacyDailyWeeklyRestCandidates); + assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges); + assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals); + assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).containsExactlyElementsOf(legacyPotentialHomeOvernightStays); + } +} diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilderTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilderTest.java new file mode 100644 index 0000000..29186d6 --- /dev/null +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/IntervalBackedDriverTimelineEventBuilderTest.java @@ -0,0 +1,179 @@ +package at.procon.eventhub.tachographfilesession.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.service.EventDetailsFactory; +import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriver; +import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard; +import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle; +import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration; +import at.procon.eventhub.tachographfilesession.model.ExtractionStats; +import at.procon.eventhub.tachographfilesession.model.TachographFileSession; +import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata; +import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class IntervalBackedDriverTimelineEventBuilderTest { + + private final IntervalBackedDriverTimelineEventBuilder builder = new IntervalBackedDriverTimelineEventBuilder( + new DriverTimelineBuilder(), + new DriverKeyFactory(), + new VehicleKeyFactory(), + new EventDetailsFactory(new ObjectMapper()) + ); + + @Test + void buildsLifecycleActivityEventsFromIntervals() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), + new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), + List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), + List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), + List.of(new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + )), + List.of(new ExtractedCardActivityInterval( + "ACT-1", + OffsetDateTime.parse("2026-05-01T08:30:00Z"), + OffsetDateTime.parse("2026-05-01T09:00:00Z"), + "DRIVE", + "DRIVER", + "INSERTED", + "SINGLE", + "12:REG-1", + "VIN-1", + "a" + )), + List.of(), + List.of() + ); + TachographFileSession session = session(driver, true); + + TachographTimelineEventBundle bundle = builder.buildEventBundle(session, driver); + + assertThat(bundle.activityEvents()).hasSize(2); + assertThat(bundle.activityEvents().get(0).eventDomain()).isEqualTo(EventDomain.DRIVER_ACTIVITY); + assertThat(bundle.activityEvents().get(0).eventType()).isEqualTo(EventType.DRIVE); + assertThat(bundle.activityEvents().get(0).lifecycle()).isEqualTo(EventLifecycle.START); + assertThat(bundle.activityEvents().get(0).occurredAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:30:00Z")); + assertThat(bundle.activityEvents().get(0).driverRef().sourceEntityId()).isEqualTo("DRV:12:123"); + assertThat(bundle.activityEvents().get(0).vehicleRef().vin()).isEqualTo("VIN-1"); + assertThat(bundle.activityEvents().get(0).eventDetails().type()).isEqualTo("DRIVER_ACTIVITY"); + assertThat(bundle.activityEvents().get(0).payload().get("raw").get("intervalId").asText()).isEqualTo("ACT-1"); + assertThat(bundle.activityEvents().get(1).lifecycle()).isEqualTo(EventLifecycle.END); + assertThat(bundle.activityEvents().get(1).occurredAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T09:00:00Z")); + } + + @Test + void buildsVehicleUsageAndSupportEvents() { + DriverExtractionSession driver = new DriverExtractionSession( + "12:123", + new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null), + new ExtractedDriverCard("CARD:12:123", "12", "123", null, null, null, null), + List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")), + List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")), + List.of(new ExtractedCardVehicleUsageInterval( + "CVU-1", + OffsetDateTime.parse("2026-05-01T08:00:00Z"), + OffsetDateTime.parse("2026-05-01T10:00:00Z"), + 100L, + 200L, + "12:REG-1", + "VIN-1", + "vu-1" + )), + List.of(), + List.of(new ExtractedSupportEvent( + "VUGNSS-1", + OffsetDateTime.parse("2026-05-01T08:45:00Z"), + "POSITION", + "GNSS_ACCUMULATED_DRIVING", + "DRIVER", + "12:REG-1", + "VIN-1", + null, + null, + BigDecimal.valueOf(48.2082), + BigDecimal.valueOf(16.3738), + "AUTHENTIC", + 150L, + null, + "raw-path" + )), + List.of() + ); + TachographFileSession session = session(driver, false); + + TachographTimelineEventBundle bundle = builder.buildEventBundle(session, driver); + + assertThat(bundle.vehicleUsageEvents()).hasSize(2); + assertThat(bundle.vehicleUsageEvents().get(0).eventDomain()).isEqualTo(EventDomain.DRIVER_CARD); + assertThat(bundle.vehicleUsageEvents().get(0).eventType()).isEqualTo(EventType.CARD_INSERTED); + assertThat(bundle.vehicleUsageEvents().get(0).lifecycle()).isEqualTo(EventLifecycle.INSERT); + assertThat(bundle.vehicleUsageEvents().get(0).odometerM()).isEqualTo(100_000L); + assertThat(bundle.vehicleUsageEvents().get(1).eventType()).isEqualTo(EventType.CARD_WITHDRAWN); + assertThat(bundle.vehicleUsageEvents().get(1).lifecycle()).isEqualTo(EventLifecycle.WITHDRAW); + assertThat(bundle.vehicleUsageEvents().get(1).odometerM()).isEqualTo(200_000L); + + assertThat(bundle.supportEvents()).hasSize(1); + assertThat(bundle.supportEvents().get(0).eventDomain()).isEqualTo(EventDomain.POSITION); + assertThat(bundle.supportEvents().get(0).eventType()).isEqualTo(EventType.POSITION_RECORDED); + assertThat(bundle.supportEvents().get(0).position().latitude()).isEqualTo(BigDecimal.valueOf(48.2082)); + assertThat(bundle.supportEvents().get(0).eventDetails().type()).isEqualTo("POSITION"); + + assertThat(bundle.allEvents()).extracting(event -> event.occurredAt()).isSorted(); + } + + private TachographFileSession session(DriverExtractionSession driver, boolean driverCardFile) { + return new TachographFileSession( + UUID.randomUUID(), + new TachographFileSessionMetadata( + "default", + "legalrequirements-drivercard", + "sample", + "sample.ddd", + "a", + 2, + "42", + "b", + driverCardFile, + null + ), + Map.of(driver.driverKey(), driver), + new ExtractionStats( + 1, + driver.cardActivityIntervals().size(), + driver.cardVehicleUsageIntervals().size(), + driver.vehicleRegistrations().size(), + driver.vehicles().size(), + 0 + ), + List.of(), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + } +} diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java index 32b0e9d..4e00cb5 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionProcessingServiceTest.java @@ -27,9 +27,11 @@ class TachographFileSessionProcessingServiceTest { void returnsEsperDriverProcessingResultsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties(); TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, - new DriverTimelineBuilder(), + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), properties ); @@ -99,9 +101,11 @@ class TachographFileSessionProcessingServiceTest { void appliesOccurredWindowToEsperDriverProcessingResults() { EventHubProperties properties = new EventHubProperties(); TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, - new DriverTimelineBuilder(), + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), properties ); @@ -191,9 +195,11 @@ class TachographFileSessionProcessingServiceTest { void returnsPotentialHomeOvernightStayIntervalsWhenVuCardAbsentCoversLongDti() { EventHubProperties properties = new EventHubProperties(); TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, - new DriverTimelineBuilder(), + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), properties ); @@ -272,9 +278,11 @@ class TachographFileSessionProcessingServiceTest { void evaluatesOperatingPeriodsFromSessionTimeline() { EventHubProperties properties = new EventHubProperties(); TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties); + DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder(); TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( repository, - new DriverTimelineBuilder(), + driverTimelineBuilder, + new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), properties );