Add rest geo evidence and boundary odometers

This commit is contained in:
trifonovt 2026-05-22 10:12:32 +02:00
parent 4535f620fc
commit f1f36e2204
14 changed files with 1894 additions and 47 deletions

View File

@ -360,6 +360,10 @@ public class EventHubProperties {
private int operatingSplitIdleHours = 7; private int operatingSplitIdleHours = 7;
private int significantDrivingMinutes = 3; private int significantDrivingMinutes = 3;
private int minimumRestPeriodMinutes = 720; private int minimumRestPeriodMinutes = 720;
private int restCandidateGeoLookbackMinutes = 180;
private int restCandidateGeoLookaheadMinutes = 180;
private int restCandidateGeoStationaryMaxMeters = 500;
private int restCandidateGeoMinorMovementMaxMeters = 2000;
private int mergeGapSeconds = 0; private int mergeGapSeconds = 0;
private int gapDetectionToleranceSeconds = 0; private int gapDetectionToleranceSeconds = 0;
@ -397,6 +401,39 @@ public class EventHubProperties {
this.minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes); this.minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes);
} }
public int getRestCandidateGeoLookbackMinutes() {
return restCandidateGeoLookbackMinutes;
}
public void setRestCandidateGeoLookbackMinutes(int restCandidateGeoLookbackMinutes) {
this.restCandidateGeoLookbackMinutes = Math.max(1, restCandidateGeoLookbackMinutes);
}
public int getRestCandidateGeoLookaheadMinutes() {
return restCandidateGeoLookaheadMinutes;
}
public void setRestCandidateGeoLookaheadMinutes(int restCandidateGeoLookaheadMinutes) {
this.restCandidateGeoLookaheadMinutes = Math.max(1, restCandidateGeoLookaheadMinutes);
}
public int getRestCandidateGeoStationaryMaxMeters() {
return restCandidateGeoStationaryMaxMeters;
}
public void setRestCandidateGeoStationaryMaxMeters(int restCandidateGeoStationaryMaxMeters) {
this.restCandidateGeoStationaryMaxMeters = Math.max(0, restCandidateGeoStationaryMaxMeters);
}
public int getRestCandidateGeoMinorMovementMaxMeters() {
return restCandidateGeoMinorMovementMaxMeters;
}
public void setRestCandidateGeoMinorMovementMaxMeters(int restCandidateGeoMinorMovementMaxMeters) {
this.restCandidateGeoMinorMovementMaxMeters =
Math.max(this.restCandidateGeoStationaryMaxMeters, restCandidateGeoMinorMovementMaxMeters);
}
public int getMergeGapSeconds() { public int getMergeGapSeconds() {
return mergeGapSeconds; return mergeGapSeconds;
} }

View File

@ -6,6 +6,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInte
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@ -32,6 +33,7 @@ public record TachographEsperDriverProcessingResultDto(
int potentialInVehicleTripIntervalCount, int potentialInVehicleTripIntervalCount,
int vehicleUsageIntervalCount, int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount, int vuCardAbsentIntervalCount,
int supportGeoEventCount,
List<TachographEsperActivityIntervalEvent> activityIntervals, List<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals, List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals, List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
@ -44,6 +46,7 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals, List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals, List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals, List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<TachographEsperSupportGeoEvent> supportGeoEvents,
List<String> notes List<String> notes
) { ) {
} }

View File

@ -18,6 +18,26 @@ public record TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
String previousRegistrationKey, String previousRegistrationKey,
String nextRegistrationKey, String nextRegistrationKey,
String previousVehicleKey, String previousVehicleKey,
String nextVehicleKey String nextVehicleKey,
Long beginBoundaryOdometerKm,
Long endBoundaryOdometerKm,
String beginGeoEventId,
String beginGeoEventDomain,
OffsetDateTime beginGeoOccurredAt,
Double beginLatitude,
Double beginLongitude,
Long beginGeoDistanceSeconds,
Long beginGeoOdometerKm,
String endGeoEventId,
String endGeoEventDomain,
OffsetDateTime endGeoOccurredAt,
Double endLatitude,
Double endLongitude,
Long endGeoDistanceSeconds,
Long endGeoOdometerKm,
Long geoEvidenceMovementMeters,
String geoEvidenceMovementCategory,
TachographEsperGeoEvidenceEvent beginGeoEvent,
TachographEsperGeoEvidenceEvent endGeoEvent
) { ) {
} }

View File

@ -0,0 +1,14 @@
package at.procon.eventhub.tachographfilesession.model;
import java.time.OffsetDateTime;
public record TachographEsperGeoEvidenceEvent(
String eventId,
String eventDomain,
OffsetDateTime occurredAt,
Double latitude,
Double longitude,
Long distanceSeconds,
Long odometerKm
) {
}

View File

@ -18,6 +18,26 @@ public record TachographEsperPotentialHomeOvernightStayIntervalEvent(
String previousRegistrationKey, String previousRegistrationKey,
String nextRegistrationKey, String nextRegistrationKey,
String previousVehicleKey, String previousVehicleKey,
String nextVehicleKey String nextVehicleKey,
Long beginBoundaryOdometerKm,
Long endBoundaryOdometerKm,
String beginGeoEventId,
String beginGeoEventDomain,
OffsetDateTime beginGeoOccurredAt,
Double beginLatitude,
Double beginLongitude,
Long beginGeoDistanceSeconds,
Long beginGeoOdometerKm,
String endGeoEventId,
String endGeoEventDomain,
OffsetDateTime endGeoOccurredAt,
Double endLatitude,
Double endLongitude,
Long endGeoDistanceSeconds,
Long endGeoOdometerKm,
Long geoEvidenceMovementMeters,
String geoEvidenceMovementCategory,
TachographEsperGeoEvidenceEvent beginGeoEvent,
TachographEsperGeoEvidenceEvent endGeoEvent
) { ) {
} }

View File

@ -18,6 +18,26 @@ public record TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
String previousRegistrationKey, String previousRegistrationKey,
String nextRegistrationKey, String nextRegistrationKey,
String previousVehicleKey, String previousVehicleKey,
String nextVehicleKey String nextVehicleKey,
Long beginBoundaryOdometerKm,
Long endBoundaryOdometerKm,
String beginGeoEventId,
String beginGeoEventDomain,
OffsetDateTime beginGeoOccurredAt,
Double beginLatitude,
Double beginLongitude,
Long beginGeoDistanceSeconds,
Long beginGeoOdometerKm,
String endGeoEventId,
String endGeoEventDomain,
OffsetDateTime endGeoOccurredAt,
Double endLatitude,
Double endLongitude,
Long endGeoDistanceSeconds,
Long endGeoOdometerKm,
Long geoEvidenceMovementMeters,
String geoEvidenceMovementCategory,
TachographEsperGeoEvidenceEvent beginGeoEvent,
TachographEsperGeoEvidenceEvent endGeoEvent
) { ) {
} }

View File

@ -0,0 +1,25 @@
package at.procon.eventhub.tachographfilesession.model;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
public record TachographEsperSupportGeoEvent(
String eventId,
String driverKey,
OffsetDateTime occurredAt,
String eventDomain,
String eventType,
String eventLifecycle,
String registrationKey,
String vehicleKey,
String country,
String region,
String countryFrom,
String countryTo,
String operation,
BigDecimal latitude,
BigDecimal longitude,
Long odometerKm,
String rawRecordPath
) {
}

View File

@ -1006,7 +1006,27 @@ public class DriverTimelineBuilder {
(String) event.get("previousRegistrationKey"), (String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"), (String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"), (String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey") (String) event.get("nextVehicleKey"),
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"UNKNOWN",
null,
null
)); ));
} }
} }

View File

@ -10,13 +10,16 @@ import com.espertech.esper.runtime.client.EPDeployException;
import com.espertech.esper.runtime.client.EPDeployment; import com.espertech.esper.runtime.client.EPDeployment;
import com.espertech.esper.runtime.client.EPRuntime; import com.espertech.esper.runtime.client.EPRuntime;
import com.espertech.esper.runtime.client.EPRuntimeProvider; import com.espertech.esper.runtime.client.EPRuntimeProvider;
import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperGeoEvidenceEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
@ -47,9 +50,14 @@ public class DriverTimelineReusableProjectionBuilder {
loadResource("esper/tachograph-driving-derived-projection-bundle.epl"); loadResource("esper/tachograph-driving-derived-projection-bundle.epl");
private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineBuilder driverTimelineBuilder;
private final EventHubProperties properties;
public DriverTimelineReusableProjectionBuilder(DriverTimelineBuilder driverTimelineBuilder) { public DriverTimelineReusableProjectionBuilder(
DriverTimelineBuilder driverTimelineBuilder,
EventHubProperties properties
) {
this.driverTimelineBuilder = driverTimelineBuilder; this.driverTimelineBuilder = driverTimelineBuilder;
this.properties = properties;
} }
public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle( public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundle(
@ -83,6 +91,7 @@ public class DriverTimelineReusableProjectionBuilder {
driverKey, driverKey,
timeline.activityIntervals(), timeline.activityIntervals(),
timeline.vehicleUsageIntervals(), timeline.vehicleUsageIntervals(),
timeline.supportEvents(),
significantDrivingMinutes, significantDrivingMinutes,
minimumRestPeriodMinutes minimumRestPeriodMinutes
); );
@ -93,6 +102,7 @@ public class DriverTimelineReusableProjectionBuilder {
String driverKey, String driverKey,
List<ResolvedActivityInterval> activityIntervals, List<ResolvedActivityInterval> activityIntervals,
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals, List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
List<ExtractedSupportEvent> supportEvents,
int significantDrivingMinutes, int significantDrivingMinutes,
int minimumRestPeriodMinutes int minimumRestPeriodMinutes
) { ) {
@ -121,6 +131,10 @@ public class DriverTimelineReusableProjectionBuilder {
"TachographVehicleUsageIntervalInputEvent", "TachographVehicleUsageIntervalInputEvent",
vehicleUsageIntervalInputDefinition() vehicleUsageIntervalInputDefinition()
); );
configuration.getCommon().addEventType(
"TachographSupportGeoEvidenceInputEvent",
supportGeoEvidenceInputDefinition()
);
}, },
renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes), renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes),
Map.of( Map.of(
@ -135,6 +149,17 @@ public class DriverTimelineReusableProjectionBuilder {
"potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals) "potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals)
), ),
runtime -> { runtime -> {
if (supportEvents != null) {
for (ExtractedSupportEvent supportEvent : supportEvents) {
Map<String, Object> supportGeoEvidence = toSupportGeoEvidenceInputMap(sessionId, supportEvent);
if (supportGeoEvidence != null) {
runtime.getEventService().sendEventMap(
supportGeoEvidence,
"TachographSupportGeoEvidenceInputEvent"
);
}
}
}
if (vehicleUsageIntervals != null) { if (vehicleUsageIntervals != null) {
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) { for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
runtime.getEventService().sendEventMap( runtime.getEventService().sendEventMap(
@ -260,6 +285,23 @@ public class DriverTimelineReusableProjectionBuilder {
return definition; return definition;
} }
private Map<String, Object> supportGeoEvidenceInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("eventId", String.class);
definition.put("eventDomain", String.class);
definition.put("occurredAt", OffsetDateTime.class);
definition.put("occurredAtEpochSecond", long.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("latitude", Double.class);
definition.put("longitude", Double.class);
definition.put("odometerKm", Long.class);
definition.put("priority", int.class);
return definition;
}
private Map<String, Object> toActivityIntervalInputMap( private Map<String, Object> toActivityIntervalInputMap(
UUID sessionId, UUID sessionId,
String driverKey, String driverKey,
@ -311,6 +353,50 @@ public class DriverTimelineReusableProjectionBuilder {
return event; return event;
} }
private Map<String, Object> toSupportGeoEvidenceInputMap(
UUID sessionId,
ExtractedSupportEvent supportEvent
) {
if (supportEvent == null
|| supportEvent.driverKey() == null
|| supportEvent.occurredAt() == null
|| supportEvent.latitude() == null
|| supportEvent.longitude() == null) {
return null;
}
int priority = supportGeoPriority(supportEvent.eventDomain());
if (priority <= 0) {
return null;
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId);
event.put("driverKey", supportEvent.driverKey());
event.put("eventId", supportEvent.eventId());
event.put("eventDomain", supportEvent.eventDomain());
event.put("occurredAt", supportEvent.occurredAt());
event.put("occurredAtEpochSecond", supportEvent.occurredAt().toEpochSecond());
event.put("registrationKey", supportEvent.registrationKey());
event.put("vehicleKey", supportEvent.vehicleKey());
event.put("latitude", supportEvent.latitude().doubleValue());
event.put("longitude", supportEvent.longitude().doubleValue());
event.put("odometerKm", supportEvent.odometerKm());
event.put("priority", priority);
return event;
}
private int supportGeoPriority(String eventDomain) {
if (eventDomain == null || eventDomain.isBlank()) {
return 0;
}
return switch (eventDomain.trim().toUpperCase()) {
case "POSITION" -> 500;
case "PLACE" -> 400;
case "BORDER_CROSSING" -> 300;
case "LOAD_UNLOAD" -> 250;
default -> 0;
};
}
private String firstSourceIntervalId(ResolvedActivityInterval interval) { private String firstSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0); return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
} }
@ -393,6 +479,24 @@ public class DriverTimelineReusableProjectionBuilder {
for (EventBean event : newData) { for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent(
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm"))
);
TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent(
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm"))
);
target.add(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( target.add(new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
(UUID) event.get("sessionId"), (UUID) event.get("sessionId"),
(String) event.get("driverKey"), (String) event.get("driverKey"),
@ -408,7 +512,27 @@ public class DriverTimelineReusableProjectionBuilder {
(String) event.get("previousRegistrationKey"), (String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"), (String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"), (String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey") (String) event.get("nextVehicleKey"),
longOrNull(event.get("beginBoundaryOdometerKm")),
longOrNull(event.get("endBoundaryOdometerKm")),
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm")),
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm")),
longOrNull(event.get("geoEvidenceMovementMeters")),
(String) event.get("geoEvidenceMovementCategory"),
beginGeoEvent,
endGeoEvent
)); ));
} }
} }
@ -423,6 +547,24 @@ public class DriverTimelineReusableProjectionBuilder {
for (EventBean event : newData) { for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent(
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm"))
);
TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent(
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm"))
);
target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent( target.add(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
(UUID) event.get("sessionId"), (UUID) event.get("sessionId"),
(String) event.get("driverKey"), (String) event.get("driverKey"),
@ -438,7 +580,27 @@ public class DriverTimelineReusableProjectionBuilder {
(String) event.get("previousRegistrationKey"), (String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"), (String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"), (String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey") (String) event.get("nextVehicleKey"),
longOrNull(event.get("beginBoundaryOdometerKm")),
longOrNull(event.get("endBoundaryOdometerKm")),
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm")),
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm")),
longOrNull(event.get("geoEvidenceMovementMeters")),
(String) event.get("geoEvidenceMovementCategory"),
beginGeoEvent,
endGeoEvent
)); ));
} }
} }
@ -453,6 +615,24 @@ public class DriverTimelineReusableProjectionBuilder {
for (EventBean event : newData) { for (EventBean event : newData) {
long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond"); long startedAtEpochSecond = (Long) event.get("startedAtEpochSecond");
long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond"); long endedAtEpochSecond = (Long) event.get("endedAtEpochSecond");
TachographEsperGeoEvidenceEvent beginGeoEvent = geoEvidenceEvent(
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm"))
);
TachographEsperGeoEvidenceEvent endGeoEvent = geoEvidenceEvent(
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm"))
);
target.add(new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( target.add(new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
(UUID) event.get("sessionId"), (UUID) event.get("sessionId"),
(String) event.get("driverKey"), (String) event.get("driverKey"),
@ -468,7 +648,27 @@ public class DriverTimelineReusableProjectionBuilder {
(String) event.get("previousRegistrationKey"), (String) event.get("previousRegistrationKey"),
(String) event.get("nextRegistrationKey"), (String) event.get("nextRegistrationKey"),
(String) event.get("previousVehicleKey"), (String) event.get("previousVehicleKey"),
(String) event.get("nextVehicleKey") (String) event.get("nextVehicleKey"),
longOrNull(event.get("beginBoundaryOdometerKm")),
longOrNull(event.get("endBoundaryOdometerKm")),
(String) event.get("beginGeoEventId"),
(String) event.get("beginGeoEventDomain"),
offsetDateTime(event.get("beginGeoOccurredAtEpochSecond")),
(Double) event.get("beginLatitude"),
(Double) event.get("beginLongitude"),
longOrNull(event.get("beginGeoDistanceSeconds")),
longOrNull(event.get("beginGeoOdometerKm")),
(String) event.get("endGeoEventId"),
(String) event.get("endGeoEventDomain"),
offsetDateTime(event.get("endGeoOccurredAtEpochSecond")),
(Double) event.get("endLatitude"),
(Double) event.get("endLongitude"),
longOrNull(event.get("endGeoDistanceSeconds")),
longOrNull(event.get("endGeoOdometerKm")),
longOrNull(event.get("geoEvidenceMovementMeters")),
(String) event.get("geoEvidenceMovementCategory"),
beginGeoEvent,
endGeoEvent
)); ));
} }
} }
@ -567,6 +767,65 @@ public class DriverTimelineReusableProjectionBuilder {
.replace( .replace(
"${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}", "${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}",
Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L) Long.toString(Math.max(1, minimumRestPeriodMinutes) * 60L)
)
.replace(
"${REST_GEO_LOOKBACK_SECONDS}",
Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookbackMinutes()) * 60L)
)
.replace(
"${REST_GEO_LOOKAHEAD_SECONDS}",
Long.toString(Math.max(1, properties.getTachographFileSession().getProcessing().getRestCandidateGeoLookaheadMinutes()) * 60L)
)
.replace(
"${REST_GEO_STATIONARY_MAX_METERS}",
Integer.toString(Math.max(0, properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters()))
)
.replace(
"${REST_GEO_MINOR_MOVEMENT_MAX_METERS}",
Integer.toString(Math.max(
properties.getTachographFileSession().getProcessing().getRestCandidateGeoStationaryMaxMeters(),
properties.getTachographFileSession().getProcessing().getRestCandidateGeoMinorMovementMaxMeters()
))
);
}
private OffsetDateTime offsetDateTime(Object epochSecond) {
if (!(epochSecond instanceof Long value)) {
return null;
}
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(value), ZoneOffset.UTC);
}
private Long longOrNull(Object value) {
return value instanceof Long number ? number : null;
}
private TachographEsperGeoEvidenceEvent geoEvidenceEvent(
String eventId,
String eventDomain,
OffsetDateTime occurredAt,
Double latitude,
Double longitude,
Long distanceSeconds,
Long odometerKm
) {
if (eventId == null
&& eventDomain == null
&& occurredAt == null
&& latitude == null
&& longitude == null
&& distanceSeconds == null
&& odometerKm == null) {
return null;
}
return new TachographEsperGeoEvidenceEvent(
eventId,
eventDomain,
occurredAt,
latitude,
longitude,
distanceSeconds,
odometerKm
); );
} }

View File

@ -6,6 +6,7 @@ import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcess
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.PeriodizedDriverActivityInterval; import at.procon.eventhub.tachographfilesession.model.PeriodizedDriverActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ProcessedDrivingInterruption; import at.procon.eventhub.tachographfilesession.model.ProcessedDrivingInterruption;
import at.procon.eventhub.tachographfilesession.model.ProcessedOperatingPeriod; import at.procon.eventhub.tachographfilesession.model.ProcessedOperatingPeriod;
@ -17,9 +18,11 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeekly
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession; import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographEsperGeoEvidenceEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import java.time.Duration; import java.time.Duration;
@ -256,6 +259,12 @@ public class TachographFileSessionProcessingService {
requestedFrom, requestedFrom,
requestedTo requestedTo
); );
List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
timeline.supportEvents(),
driverKey,
requestedFrom,
requestedTo
);
return new TachographEsperDriverProcessingResultDto( return new TachographEsperDriverProcessingResultDto(
sessionId, sessionId,
@ -277,8 +286,9 @@ public class TachographFileSessionProcessingService {
potentialInVehicleTripIntervals.size(), potentialInVehicleTripIntervals.size(),
vehicleUsageIntervals.size(), vehicleUsageIntervals.size(),
vuCardAbsentIntervals.size(), vuCardAbsentIntervals.size(),
List.of()/*activityIntervals*/, supportGeoEvents.size(),
List.of()/*drivingIntervals*/, activityIntervals,
drivingIntervals,
drivingInterruptionIntervals, drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals, drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals, dailyWeeklyRestCandidateIntervals,
@ -289,6 +299,7 @@ public class TachographFileSessionProcessingService {
potentialInVehicleTripIntervals, potentialInVehicleTripIntervals,
vehicleUsageIntervals, vehicleUsageIntervals,
vuCardAbsentIntervals, vuCardAbsentIntervals,
supportGeoEvents,
esperProjectionNotes() esperProjectionNotes()
); );
} }
@ -388,6 +399,45 @@ public class TachographFileSessionProcessingService {
.toList(); .toList();
} }
private List<TachographEsperSupportGeoEvent> clipEsperSupportGeoEvents(
List<ExtractedSupportEvent> supportEvents,
String driverKey,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo
) {
if (supportEvents == null || supportEvents.isEmpty() || requestedFrom == null || requestedTo == null) {
return List.of();
}
return supportEvents.stream()
.filter(event -> Objects.equals(driverKey, event.driverKey()))
.filter(event -> event.occurredAt() != null)
.filter(event -> event.latitude() != null && event.longitude() != null)
.filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo))
.map(event -> new TachographEsperSupportGeoEvent(
event.eventId(),
event.driverKey(),
event.occurredAt(),
event.eventDomain(),
event.eventType(),
event.eventLifecycle(),
event.registrationKey(),
event.vehicleKey(),
event.country(),
event.region(),
event.countryFrom(),
event.countryTo(),
event.operation(),
event.latitude(),
event.longitude(),
event.odometerKm(),
event.rawRecordPath()
))
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
.thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents( private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
List<TachographEsperDrivingInterruptionIntervalEvent> intervals, List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
OffsetDateTime requestedFrom, OffsetDateTime requestedFrom,
@ -495,6 +545,8 @@ public class TachographFileSessionProcessingService {
double unknownCoveragePercent = durationSeconds == 0L double unknownCoveragePercent = durationSeconds == 0L
? 0.0d ? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds; : (unknownDurationSeconds * 100.0d) / durationSeconds;
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
boolean endBoundaryChanged = !end.equals(interval.endedAt());
return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent( return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
interval.sessionId(), interval.sessionId(),
interval.driverKey(), interval.driverKey(),
@ -510,7 +562,25 @@ public class TachographFileSessionProcessingService {
interval.previousRegistrationKey(), interval.previousRegistrationKey(),
interval.nextRegistrationKey(), interval.nextRegistrationKey(),
interval.previousVehicleKey(), interval.previousVehicleKey(),
interval.nextVehicleKey() interval.nextVehicleKey(),
beginBoundaryChanged ? null : interval.beginGeoEventId(),
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
beginBoundaryChanged ? null : interval.beginLatitude(),
beginBoundaryChanged ? null : interval.beginLongitude(),
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
endBoundaryChanged ? null : interval.endGeoEventId(),
endBoundaryChanged ? null : interval.endGeoEventDomain(),
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
endBoundaryChanged ? null : interval.endLatitude(),
endBoundaryChanged ? null : interval.endLongitude(),
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
); );
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -556,6 +626,8 @@ public class TachographFileSessionProcessingService {
double unknownCoveragePercent = durationSeconds == 0L double unknownCoveragePercent = durationSeconds == 0L
? 0.0d ? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds; : (unknownDurationSeconds * 100.0d) / durationSeconds;
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
boolean endBoundaryChanged = !end.equals(interval.endedAt());
return new TachographEsperPotentialHomeOvernightStayIntervalEvent( return new TachographEsperPotentialHomeOvernightStayIntervalEvent(
interval.sessionId(), interval.sessionId(),
interval.driverKey(), interval.driverKey(),
@ -571,7 +643,25 @@ public class TachographFileSessionProcessingService {
interval.previousRegistrationKey(), interval.previousRegistrationKey(),
interval.nextRegistrationKey(), interval.nextRegistrationKey(),
interval.previousVehicleKey(), interval.previousVehicleKey(),
interval.nextVehicleKey() interval.nextVehicleKey(),
beginBoundaryChanged ? null : interval.beginGeoEventId(),
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
beginBoundaryChanged ? null : interval.beginLatitude(),
beginBoundaryChanged ? null : interval.beginLongitude(),
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
endBoundaryChanged ? null : interval.endGeoEventId(),
endBoundaryChanged ? null : interval.endGeoEventDomain(),
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
endBoundaryChanged ? null : interval.endLatitude(),
endBoundaryChanged ? null : interval.endLongitude(),
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
); );
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -617,6 +707,8 @@ public class TachographFileSessionProcessingService {
double unknownCoveragePercent = durationSeconds == 0L double unknownCoveragePercent = durationSeconds == 0L
? 0.0d ? 0.0d
: (unknownDurationSeconds * 100.0d) / durationSeconds; : (unknownDurationSeconds * 100.0d) / durationSeconds;
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
boolean endBoundaryChanged = !end.equals(interval.endedAt());
return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent( return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
interval.sessionId(), interval.sessionId(),
interval.driverKey(), interval.driverKey(),
@ -632,7 +724,25 @@ public class TachographFileSessionProcessingService {
interval.previousRegistrationKey(), interval.previousRegistrationKey(),
interval.nextRegistrationKey(), interval.nextRegistrationKey(),
interval.previousVehicleKey(), interval.previousVehicleKey(),
interval.nextVehicleKey() interval.nextVehicleKey(),
beginBoundaryChanged ? null : interval.beginGeoEventId(),
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
beginBoundaryChanged ? null : interval.beginLatitude(),
beginBoundaryChanged ? null : interval.beginLongitude(),
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
endBoundaryChanged ? null : interval.endGeoEventId(),
endBoundaryChanged ? null : interval.endGeoEventDomain(),
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
endBoundaryChanged ? null : interval.endLatitude(),
endBoundaryChanged ? null : interval.endLongitude(),
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
); );
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -1300,6 +1410,9 @@ public class TachographFileSessionProcessingService {
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.", "Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
"Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.", "Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
"Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and unknown-coverage metrics computed from vehicle-usage and VU card-absent overlap.", "Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and unknown-coverage metrics computed from vehicle-usage and VU card-absent overlap.",
"Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.",
"Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.",
"If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.",
"Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.", "Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.", "Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
"Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.", "Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
@ -1367,6 +1480,35 @@ public class TachographFileSessionProcessingService {
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC); return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
} }
private TachographEsperGeoEvidenceEvent geoEvidenceEvent(
String eventId,
String eventDomain,
OffsetDateTime occurredAt,
Double latitude,
Double longitude,
Long distanceSeconds,
Long odometerKm
) {
if (eventId == null
&& eventDomain == null
&& occurredAt == null
&& latitude == null
&& longitude == null
&& distanceSeconds == null
&& odometerKm == null) {
return null;
}
return new TachographEsperGeoEvidenceEvent(
eventId,
eventDomain,
occurredAt,
latitude,
longitude,
distanceSeconds,
odometerKm
);
}
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) { private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
if (left == null) { if (left == null) {
return right; return right;

View File

@ -24,6 +24,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeekly
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
@ -93,6 +94,7 @@ class TachographFileSessionControllerTest {
0, 0,
2, 2,
1, 1,
1,
List.of(new TachographEsperActivityIntervalEvent( List.of(new TachographEsperActivityIntervalEvent(
sessionId, sessionId,
"12:123", "12:123",
@ -185,7 +187,27 @@ class TachographFileSessionControllerTest {
"12:REG-1", "12:REG-1",
"12:REG-2", "12:REG-2",
"VIN-1", "VIN-1",
"VIN-2" "VIN-2",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"UNKNOWN",
null,
null
)), )),
List.of(), List.of(),
List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent( List.of(new TachographEsperPotentialHomeOvernightStayIntervalEvent(
@ -203,7 +225,27 @@ class TachographFileSessionControllerTest {
"12:REG-1", "12:REG-1",
"12:REG-2", "12:REG-2",
"VIN-1", "VIN-1",
"VIN-2" "VIN-2",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"UNKNOWN",
null,
null
)), )),
List.of(), List.of(),
List.<TachographEsperPotentialInVehicleTripIntervalEvent>of(), List.<TachographEsperPotentialInVehicleTripIntervalEvent>of(),
@ -234,6 +276,25 @@ class TachographFileSessionControllerTest {
"VIN-1", "VIN-1",
"VIN-2" "VIN-2"
)), )),
List.of(new TachographEsperSupportGeoEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-12T10:15:00Z"),
"POSITION",
"POSITION_RECORDED",
"SNAPSHOT",
"12:REG-1",
"VIN-1",
null,
null,
null,
null,
null,
java.math.BigDecimal.valueOf(48.2082),
java.math.BigDecimal.valueOf(16.3738),
123L,
"path"
)),
List.of("note") List.of("note")
)); ));
when(processingService.evaluateOperatingPeriods(eq(sessionId), eq("12:123"), org.mockito.ArgumentMatchers.any(TachographOperatingPeriodsProcessingRequest.class))) when(processingService.evaluateOperatingPeriods(eq(sessionId), eq("12:123"), org.mockito.ArgumentMatchers.any(TachographOperatingPeriodsProcessingRequest.class)))
@ -307,6 +368,8 @@ class TachographFileSessionControllerTest {
.andExpect(jsonPath("$.potentialInVehicleOvernightStayIntervalCount").value(0)) .andExpect(jsonPath("$.potentialInVehicleOvernightStayIntervalCount").value(0))
.andExpect(jsonPath("$.potentialInVehicleTripIntervalCount").value(0)) .andExpect(jsonPath("$.potentialInVehicleTripIntervalCount").value(0))
.andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1)) .andExpect(jsonPath("$.vuCardAbsentIntervalCount").value(1))
.andExpect(jsonPath("$.supportGeoEventCount").value(1))
.andExpect(jsonPath("$.supportGeoEvents[0].eventId").value("SUP-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousRegistrationKey").value("12:REG-1"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].nextRegistrationKey").value("12:REG-2"))
.andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousVehicleKey").value("VIN-1")) .andExpect(jsonPath("$.drivingInterruptionIntervals[0].previousVehicleKey").value("VIN-1"))

View File

@ -1,10 +1,12 @@
package at.procon.eventhub.tachographfilesession.service; package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession; import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval; import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval; import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats; import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
@ -26,7 +28,7 @@ class DriverTimelineReusableProjectionBuilderTest {
private final DriverTimelineBuilder legacyBuilder = new DriverTimelineBuilder(); private final DriverTimelineBuilder legacyBuilder = new DriverTimelineBuilder();
private final DriverTimelineReusableProjectionBuilder reusableBuilder = private final DriverTimelineReusableProjectionBuilder reusableBuilder =
new DriverTimelineReusableProjectionBuilder(legacyBuilder); new DriverTimelineReusableProjectionBuilder(legacyBuilder, new EventHubProperties());
@Test @Test
void matchesLegacyDrivingDerivedProjectionChain() { void matchesLegacyDrivingDerivedProjectionChain() {
@ -130,7 +132,17 @@ class DriverTimelineReusableProjectionBuilderTest {
assertThat(coverageInterval.cardPresentCoveragePercent()).isEqualTo(0.0d); assertThat(coverageInterval.cardPresentCoveragePercent()).isEqualTo(0.0d);
assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges); assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges);
assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals); assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals);
assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).containsExactlyElementsOf(legacyPotentialHomeOvernightStays); assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).hasSize(1);
assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).startedAt())
.isEqualTo(legacyPotentialHomeOvernightStays.get(0).startedAt());
assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endedAt())
.isEqualTo(legacyPotentialHomeOvernightStays.get(0).endedAt());
assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).unknownCoveragePercent())
.isEqualTo(legacyPotentialHomeOvernightStays.get(0).unknownCoveragePercent());
assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).beginBoundaryOdometerKm())
.isEqualTo(200L);
assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endBoundaryOdometerKm())
.isEqualTo(201L);
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).isEmpty(); assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).isEmpty();
} }
@ -180,7 +192,56 @@ class DriverTimelineReusableProjectionBuilderTest {
"b" "b"
) )
), ),
List.of(), List.of(
new ExtractedSupportEvent(
"SUP-1",
"12:123",
OffsetDateTime.parse("2026-05-01T09:45:00Z"),
"POSITION",
"POSITION_RECORDED",
"SNAPSHOT",
null,
"12:REG-1",
"VIN-1",
null,
null,
null,
null,
null,
new java.math.BigDecimal("48.2082"),
new java.math.BigDecimal("16.3738"),
null,
150L,
null,
null,
null,
"geo-1"
),
new ExtractedSupportEvent(
"SUP-2",
"12:123",
OffsetDateTime.parse("2026-05-02T00:15:00Z"),
"POSITION",
"POSITION_RECORDED",
"SNAPSHOT",
null,
"12:REG-1",
"VIN-1",
null,
null,
null,
null,
null,
new java.math.BigDecimal("48.2090"),
new java.math.BigDecimal("16.3740"),
null,
150L,
null,
null,
null,
"geo-2"
)
),
List.of() List.of()
); );
TachographFileSession session = new TachographFileSession( TachographFileSession session = new TachographFileSession(
@ -209,6 +270,26 @@ class DriverTimelineReusableProjectionBuilderTest {
.isEqualTo(100.0d); .isEqualTo(100.0d);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent()) assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).unknownCoveragePercent())
.isEqualTo(0.0d); .isEqualTo(0.0d);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoEventId())
.isEqualTo("SUP-1");
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoEvent())
.extracting("eventId", "eventDomain")
.containsExactly("SUP-1", "POSITION");
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoEventId())
.isEqualTo("SUP-2");
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoEvent())
.extracting("eventId", "eventDomain")
.containsExactly("SUP-2", "POSITION");
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoDistanceSeconds())
.isEqualTo(900L);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoDistanceSeconds())
.isEqualTo(900L);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginBoundaryOdometerKm())
.isEqualTo(100L);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endBoundaryOdometerKm())
.isEqualTo(260L);
assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).geoEvidenceMovementCategory())
.isEqualTo("STATIONARY");
assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).isEmpty(); assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).isEmpty();
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).hasSize(1); assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).hasSize(1);
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).startedAt()) assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).startedAt())
@ -217,5 +298,10 @@ class DriverTimelineReusableProjectionBuilderTest {
.isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z")); .isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent()) assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).cardPresentCoveragePercent())
.isEqualTo(100.0d); .isEqualTo(100.0d);
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEventId())
.isEqualTo("SUP-1");
assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEvent())
.extracting("eventId", "eventDomain")
.containsExactly("SUP-1", "POSITION");
} }
} }

View File

@ -33,7 +33,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -119,7 +119,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -226,7 +226,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -315,7 +315,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -417,7 +417,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -502,7 +502,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -594,7 +594,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,
@ -668,7 +668,7 @@ class TachographFileSessionProcessingServiceTest {
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService( TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
repository, repository,
driverTimelineBuilder, driverTimelineBuilder,
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder), new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder, new EventHubProperties()),
new EventBackedDriverTimelineBuilder( new EventBackedDriverTimelineBuilder(
new IntervalBackedDriverTimelineEventBuilder( new IntervalBackedDriverTimelineEventBuilder(
driverTimelineBuilder, driverTimelineBuilder,