Add reusable tachograph projections and event adapter
This commit is contained in:
parent
317983eba8
commit
9ef8bfc412
|
|
@ -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
|
||||
) {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue