Add reusable tachograph projections and event adapter

This commit is contained in:
trifonovt 2026-05-19 16:21:26 +02:00
parent 317983eba8
commit 9ef8bfc412
10 changed files with 1728 additions and 19 deletions

View File

@ -0,0 +1,12 @@
package at.procon.eventhub.tachographfilesession.model;
import java.util.List;
public record TachographEsperDrivingDerivedProjectionBundle(
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals
) {
}

View File

@ -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<EventHubEventDto> activityEvents,
List<EventHubEventDto> vehicleUsageEvents,
List<EventHubEventDto> supportEvents
) {
public TachographTimelineEventBundle {
activityEvents = copy(activityEvents);
vehicleUsageEvents = copy(vehicleUsageEvents);
supportEvents = copy(supportEvents);
}
public List<EventHubEventDto> allEvents() {
List<EventHubEventDto> 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<EventHubEventDto> copy(List<EventHubEventDto> events) {
return events == null ? List.of() : List.copyOf(events);
}
}

View File

@ -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<EventHubEventDto> buildEvents(
TachographFileSession session,
DriverExtractionSession driverSession
) {
return buildEventBundle(session, driverSession).allEvents();
}
default List<EventHubEventDto> buildEvents(
TachographFileSession session,
DriverExtractionSession driverSession,
ResolvedDriverTimeline timeline
) {
return buildEventBundle(session, driverSession, timeline).allEvents();
}
}

View File

@ -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<ResolvedActivityInterval> activityIntervals,
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
if ((activityIntervals == null || activityIntervals.isEmpty())
&& (vehicleUsageIntervals == null || vehicleUsageIntervals.isEmpty())) {
return emptyBundle();
}
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = new ArrayList<>();
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals = new ArrayList<>();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = new ArrayList<>();
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = new ArrayList<>();
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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<Configuration> configurationSetup,
String epl,
Map<String, Consumer<EventBean[]>> listeners,
Consumer<EPRuntime> 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<String, Consumer<EventBean[]>> 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<String, Object> activityIntervalInputDefinition() {
Map<String, Object> 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<String, Object> vehicleUsageIntervalInputDefinition() {
Map<String, Object> 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<String, Object> toActivityIntervalInputMap(
UUID sessionId,
String driverKey,
ResolvedActivityInterval interval
) {
Map<String, Object> 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<String, Object> toVehicleUsageIntervalInputMap(ResolvedVehicleUsageInterval interval) {
Map<String, Object> 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<TachographEsperDrivingInterruptionIntervalEvent> 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<TachographEsperVuCardAbsentIntervalEvent> 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<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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<TachographEsperDrivingInterruptionIntervalEvent> sortDrivingInterruptionIntervals(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals
) {
return intervals.stream()
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperVuCardAbsentIntervalEvent> sortVuCardAbsentIntervals(
List<TachographEsperVuCardAbsentIntervalEvent> intervals
) {
return intervals.stream()
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
.toList();
}
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> sortPotentialHomeOvernightStayIntervals(
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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);
}
}
}

View File

@ -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<String, ExtractedVehicleRegistration> registrationsByKey = new LinkedHashMap<>();
for (ExtractedVehicleRegistration registration : driverSession.vehicleRegistrations()) {
registrationsByKey.put(registration.registrationKey(), registration);
}
Map<String, ExtractedVehicle> 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<EventHubEventDto> activityEvents = buildActivityEvents(
session,
timeline.activityIntervals(),
driverRef,
registrationsByKey,
vehiclesByKey,
eventSource,
sourcePackageRef
);
List<EventHubEventDto> vehicleUsageEvents = buildVehicleUsageEvents(
session,
timeline.vehicleUsageIntervals(),
driverRef,
registrationsByKey,
vehiclesByKey,
eventSource,
sourcePackageRef
);
List<EventHubEventDto> supportEvents = buildSupportEvents(
session,
timeline.supportEvents(),
driverRef,
registrationsByKey,
vehiclesByKey,
eventSource,
sourcePackageRef
);
return new TachographTimelineEventBundle(activityEvents, vehicleUsageEvents, supportEvents);
}
private List<EventHubEventDto> buildActivityEvents(
TachographFileSession session,
List<ResolvedActivityInterval> intervals,
DriverRefDto driverRef,
Map<String, ExtractedVehicleRegistration> registrationsByKey,
Map<String, ExtractedVehicle> vehiclesByKey,
EventSourceDto eventSource,
SourcePackageRefDto sourcePackageRef
) {
if (intervals == null || intervals.isEmpty()) {
return List.of();
}
List<EventHubEventDto> 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<String, Object> 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<EventHubEventDto> buildVehicleUsageEvents(
TachographFileSession session,
List<ResolvedVehicleUsageInterval> intervals,
DriverRefDto driverRef,
Map<String, ExtractedVehicleRegistration> registrationsByKey,
Map<String, ExtractedVehicle> vehiclesByKey,
EventSourceDto eventSource,
SourcePackageRefDto sourcePackageRef
) {
if (intervals == null || intervals.isEmpty()) {
return List.of();
}
List<EventHubEventDto> 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<String, Object> 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<EventHubEventDto> buildSupportEvents(
TachographFileSession session,
List<ExtractedSupportEvent> supportEvents,
DriverRefDto driverRef,
Map<String, ExtractedVehicleRegistration> registrationsByKey,
Map<String, ExtractedVehicle> vehiclesByKey,
EventSourceDto eventSource,
SourcePackageRefDto sourcePackageRef
) {
if (supportEvents == null || supportEvents.isEmpty()) {
return List.of();
}
List<EventHubEventDto> 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<String, Object> 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<String, Object> 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<String, ExtractedVehicleRegistration> registrationsByKey,
Map<String, ExtractedVehicle> 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 extends Enum<T>> T parseEnum(Class<T> 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;
}
}
}

View File

@ -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<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionIntervalEvents(
TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle =
reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
sessionId,
driverKey,
timeline,
significantDrivingMinutes
significantDrivingMinutes,
minimumRestPeriodMinutes
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
derivedProjectionBundle.drivingInterruptionIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDrivingInterruptionIntervals,
@ -175,10 +182,7 @@ public class TachographFileSessionProcessingService {
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(
rawDrivingInterruptionIntervals,
minimumRestPeriodMinutes
);
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDailyWeeklyRestCandidateIntervals,
@ -186,9 +190,7 @@ public class TachographFileSessionProcessingService {
requestedTo
);
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
rawDailyWeeklyRestCandidateIntervals
);
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
clipEsperDrivingInterruptionIntervalEvents(
rawDrivingInterruptionVehicleChangeIntervals,
@ -196,13 +198,10 @@ public class TachographFileSessionProcessingService {
requestedTo
);
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
driverTimelineBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);
derivedProjectionBundle.vuCardAbsentIntervals();
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
clipEsperPotentialHomeOvernightStayIntervalEvents(
driverTimelineBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
rawDrivingInterruptionVehicleChangeIntervals,
rawVuCardAbsentIntervals
),
derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
rawVuCardAbsentIntervals,
requestedFrom,
requestedTo

View File

@ -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;

View File

@ -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<TachographEsperDrivingInterruptionIntervalEvent> legacyDrivingInterruptions =
legacyBuilder.buildEsperDrivingInterruptionIntervalEvents(session.sessionId(), driver.driverKey(), timeline, 3);
List<TachographEsperDrivingInterruptionIntervalEvent> legacyDailyWeeklyRestCandidates =
legacyBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(legacyDrivingInterruptions, 720);
List<TachographEsperDrivingInterruptionIntervalEvent> legacyDrivingInterruptionVehicleChanges =
legacyBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(legacyDailyWeeklyRestCandidates);
List<TachographEsperVuCardAbsentIntervalEvent> legacyVuCardAbsentIntervals =
legacyBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> 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);
}
}

View File

@ -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)
);
}
}

View File

@ -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
);