Compare commits
4 Commits
317983eba8
...
04d7bf513e
| Author | SHA1 | Date |
|---|---|---|
|
|
04d7bf513e | |
|
|
dd0ccae290 | |
|
|
2ded38a28a | |
|
|
9ef8bfc412 |
|
|
@ -356,12 +356,23 @@ public class EventHubProperties {
|
|||
}
|
||||
|
||||
public static class Processing {
|
||||
private TimelineInputMode timelineInputMode = TimelineInputMode.INTERVALS;
|
||||
private int operatingSplitIdleHours = 7;
|
||||
private int significantDrivingMinutes = 3;
|
||||
private int minimumRestPeriodMinutes = 720;
|
||||
private int mergeGapSeconds = 0;
|
||||
private int gapDetectionToleranceSeconds = 0;
|
||||
|
||||
public TimelineInputMode getTimelineInputMode() {
|
||||
return timelineInputMode;
|
||||
}
|
||||
|
||||
public void setTimelineInputMode(TimelineInputMode timelineInputMode) {
|
||||
if (timelineInputMode != null) {
|
||||
this.timelineInputMode = timelineInputMode;
|
||||
}
|
||||
}
|
||||
|
||||
public int getOperatingSplitIdleHours() {
|
||||
return operatingSplitIdleHours;
|
||||
}
|
||||
|
|
@ -403,6 +414,11 @@ public class EventHubProperties {
|
|||
}
|
||||
}
|
||||
|
||||
public enum TimelineInputMode {
|
||||
INTERVALS,
|
||||
EVENTS
|
||||
}
|
||||
|
||||
public static class LegalRequirements {
|
||||
private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
package at.procon.eventhub.persistence;
|
||||
|
||||
import at.procon.eventhub.dto.DriverCardRefDto;
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
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.ImportScopeType;
|
||||
import at.procon.eventhub.dto.SourceGroupRefDto;
|
||||
import at.procon.eventhub.dto.SourceGroupType;
|
||||
import at.procon.eventhub.dto.SourcePackageRefDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class EventHubEventReadRepository {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public EventHubEventReadRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public List<EventHubEventDto> findEvents(
|
||||
UnifiedDriverEventsRequest request,
|
||||
String providerKey,
|
||||
List<String> sourceKinds
|
||||
) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"""
|
||||
select
|
||||
event.id,
|
||||
event.external_source_event_id,
|
||||
event.occurred_at,
|
||||
event.received_partner_at,
|
||||
event.received_hub_at,
|
||||
event.event_domain,
|
||||
event.event_type,
|
||||
event.lifecycle,
|
||||
event.odometer_m,
|
||||
ST_Y(event.position::geometry) as latitude,
|
||||
ST_X(event.position::geometry) as longitude,
|
||||
event.payload,
|
||||
event.manual_entry,
|
||||
source.provider_key,
|
||||
source.source_kind,
|
||||
source.source_key,
|
||||
source.source_instance_key,
|
||||
source.tenant_provider_setting_key,
|
||||
source.external_fleet_key,
|
||||
package.id as data_package_id,
|
||||
package.tenant_key,
|
||||
package.event_family,
|
||||
package.business_date,
|
||||
package.external_package_id,
|
||||
package.source_group_type,
|
||||
package.source_group_entity_id,
|
||||
package.source_group_code,
|
||||
package.source_group_name,
|
||||
package.import_scope_type,
|
||||
package.root_source_org_entity_id,
|
||||
package.root_source_org_code,
|
||||
package.root_source_org_name,
|
||||
package.include_children,
|
||||
package.occurred_from as package_occurred_from,
|
||||
package.occurred_to as package_occurred_to,
|
||||
package.source_package_kind as package_source_package_kind,
|
||||
package.source_package_period_from,
|
||||
package.source_package_period_to,
|
||||
package.source_package_imported_at,
|
||||
event.source_package_id,
|
||||
event.source_package_entity_id,
|
||||
detail.detail_type,
|
||||
detail.attributes,
|
||||
driver.source_driver_entity_id,
|
||||
driver_card.nation as driver_card_nation,
|
||||
driver_card.card_number as driver_card_number,
|
||||
vehicle.source_vehicle_entity_id,
|
||||
vehicle.vin,
|
||||
registration.source_registration_entity_id,
|
||||
registration.nation as vehicle_registration_nation,
|
||||
registration.registration_number as vehicle_registration_number,
|
||||
event.driver_id,
|
||||
event.vehicle_id,
|
||||
event.vehicle_registration_id
|
||||
from eventhub.event event
|
||||
join eventhub.event_source source on source.id = event.event_source_id
|
||||
join eventhub.data_package package on package.id = event.data_package_id
|
||||
left join lateral (
|
||||
select detail_type, attributes
|
||||
from eventhub.event_detail detail
|
||||
where detail.event_occurred_at = event.occurred_at
|
||||
and detail.event_id = event.id
|
||||
order by detail_type
|
||||
limit 1
|
||||
) detail on true
|
||||
left join eventhub.driver driver on driver.id = event.driver_id
|
||||
left join eventhub.driver_card driver_card on driver_card.id = event.driver_card_id
|
||||
left join eventhub.vehicle vehicle on vehicle.id = event.vehicle_id
|
||||
left join eventhub.vehicle_registration registration on registration.id = event.vehicle_registration_id
|
||||
where package.tenant_key = ?
|
||||
and source.provider_key = ?
|
||||
"""
|
||||
);
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(request.tenantKey());
|
||||
params.add(providerKey);
|
||||
|
||||
if (sourceKinds != null && !sourceKinds.isEmpty()) {
|
||||
sql.append(" and source.source_kind in (");
|
||||
for (int i = 0; i < sourceKinds.size(); i++) {
|
||||
if (i > 0) {
|
||||
sql.append(", ");
|
||||
}
|
||||
sql.append("?");
|
||||
params.add(sourceKinds.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
if (request.occurredFrom() != null) {
|
||||
sql.append(" and event.occurred_at >= ?");
|
||||
params.add(request.occurredFrom());
|
||||
}
|
||||
if (request.occurredTo() != null) {
|
||||
sql.append(" and event.occurred_at <= ?");
|
||||
params.add(request.occurredTo());
|
||||
}
|
||||
if (request.driverSourceEntityId() != null) {
|
||||
sql.append(" and driver.source_driver_entity_id = ?");
|
||||
params.add(request.driverSourceEntityId());
|
||||
}
|
||||
if (request.driverCardNumber() != null) {
|
||||
sql.append(" and driver_card.card_number = ?");
|
||||
params.add(request.driverCardNumber());
|
||||
if (request.driverCardNation() != null) {
|
||||
sql.append(" and driver_card.nation = ?");
|
||||
params.add(request.driverCardNation());
|
||||
}
|
||||
}
|
||||
if (request.vehicleSourceEntityId() != null) {
|
||||
sql.append(" and vehicle.source_vehicle_entity_id = ?");
|
||||
params.add(request.vehicleSourceEntityId());
|
||||
}
|
||||
if (request.vin() != null) {
|
||||
sql.append(" and vehicle.vin = ?");
|
||||
params.add(request.vin());
|
||||
}
|
||||
if (request.registrationNumber() != null) {
|
||||
sql.append(" and registration.registration_number = ?");
|
||||
params.add(request.registrationNumber());
|
||||
if (request.registrationNation() != null) {
|
||||
sql.append(" and registration.nation = ?");
|
||||
params.add(request.registrationNation());
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(" order by event.occurred_at, event.event_domain, event.event_type, event.lifecycle, event.id");
|
||||
|
||||
return jdbcTemplate.query(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> mapEvent(rs),
|
||||
params.toArray()
|
||||
);
|
||||
}
|
||||
|
||||
private EventHubEventDto mapEvent(ResultSet rs) throws SQLException {
|
||||
DriverRefDto driverRef = driverRef(rs);
|
||||
VehicleRefDto vehicleRef = vehicleRef(rs);
|
||||
if ((driverRef == null || !driverRef.hasAnyReference()) && (vehicleRef == null || !vehicleRef.hasAnyReference())) {
|
||||
throw new IllegalStateException("Loaded event does not have any driver or vehicle reference.");
|
||||
}
|
||||
|
||||
return new EventHubEventDto(
|
||||
uuid(rs, "id"),
|
||||
rs.getString("external_source_event_id"),
|
||||
driverRef,
|
||||
vehicleRef,
|
||||
rs.getObject("occurred_at", OffsetDateTime.class),
|
||||
rs.getObject("received_partner_at", OffsetDateTime.class),
|
||||
rs.getObject("received_hub_at", OffsetDateTime.class),
|
||||
enumValue(EventDomain.class, rs.getString("event_domain"), EventDomain.TELEMATICS_DATA),
|
||||
enumValue(EventType.class, rs.getString("event_type"), EventType.UNKNOWN_EVENT),
|
||||
enumValue(EventLifecycle.class, rs.getString("lifecycle"), EventLifecycle.SNAPSHOT),
|
||||
longValue(rs, "odometer_m"),
|
||||
point(rs),
|
||||
eventDetails(rs),
|
||||
sourcePackageRef(rs),
|
||||
json(rs.getString("payload")),
|
||||
rs.getBoolean("manual_entry"),
|
||||
packageInfo(rs)
|
||||
);
|
||||
}
|
||||
|
||||
private DriverRefDto driverRef(ResultSet rs) throws SQLException {
|
||||
String sourceEntityId = firstNonBlank(
|
||||
rs.getString("source_driver_entity_id"),
|
||||
syntheticId("EVENTHUB_DRIVER", uuid(rs, "driver_id"))
|
||||
);
|
||||
String cardNumber = rs.getString("driver_card_number");
|
||||
DriverCardRefDto driverCard = cardNumber == null
|
||||
? null
|
||||
: new DriverCardRefDto(rs.getString("driver_card_nation"), cardNumber);
|
||||
DriverRefDto driverRef = new DriverRefDto(sourceEntityId, driverCard);
|
||||
return driverRef.hasAnyReference() ? driverRef : null;
|
||||
}
|
||||
|
||||
private VehicleRefDto vehicleRef(ResultSet rs) throws SQLException {
|
||||
VehicleRegistrationRefDto registration = null;
|
||||
String registrationNumber = rs.getString("vehicle_registration_number");
|
||||
if (registrationNumber != null) {
|
||||
registration = new VehicleRegistrationRefDto(rs.getString("vehicle_registration_nation"), registrationNumber);
|
||||
}
|
||||
VehicleRefDto vehicleRef = new VehicleRefDto(
|
||||
firstNonBlank(
|
||||
rs.getString("source_vehicle_entity_id"),
|
||||
syntheticId("EVENTHUB_VEHICLE", uuid(rs, "vehicle_id"))
|
||||
),
|
||||
rs.getString("vin"),
|
||||
firstNonBlank(
|
||||
rs.getString("source_registration_entity_id"),
|
||||
syntheticId("EVENTHUB_VEHICLE_REGISTRATION", uuid(rs, "vehicle_registration_id"))
|
||||
),
|
||||
registration
|
||||
);
|
||||
return vehicleRef.hasAnyReference() ? vehicleRef : null;
|
||||
}
|
||||
|
||||
private EventDetailsDto eventDetails(ResultSet rs) throws SQLException {
|
||||
String detailType = rs.getString("detail_type");
|
||||
if (detailType == null || detailType.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return new EventDetailsDto(detailType, json(rs.getString("attributes")));
|
||||
}
|
||||
|
||||
private SourcePackageRefDto sourcePackageRef(ResultSet rs) throws SQLException {
|
||||
String packageKind = rs.getString("package_source_package_kind");
|
||||
String sourcePackageId = firstNonBlank(rs.getString("source_package_id"), null);
|
||||
String sourceEntityId = firstNonBlank(rs.getString("source_package_entity_id"), null);
|
||||
OffsetDateTime periodFrom = rs.getObject("source_package_period_from", OffsetDateTime.class);
|
||||
OffsetDateTime periodTo = rs.getObject("source_package_period_to", OffsetDateTime.class);
|
||||
OffsetDateTime importedAt = rs.getObject("source_package_imported_at", OffsetDateTime.class);
|
||||
SourcePackageRefDto ref = new SourcePackageRefDto(
|
||||
packageKind,
|
||||
sourcePackageId,
|
||||
sourceEntityId,
|
||||
periodFrom,
|
||||
periodTo,
|
||||
importedAt
|
||||
);
|
||||
return ref.hasAnyReference() ? ref : null;
|
||||
}
|
||||
|
||||
private EventHubPackageRequest packageInfo(ResultSet rs) throws SQLException {
|
||||
EventSourceDto eventSource = new EventSourceDto(
|
||||
rs.getString("provider_key"),
|
||||
rs.getString("source_kind"),
|
||||
rs.getString("source_key"),
|
||||
rs.getString("source_instance_key"),
|
||||
rs.getString("tenant_provider_setting_key"),
|
||||
rs.getString("external_fleet_key")
|
||||
);
|
||||
SourceGroupRefDto sourceGroup = sourceGroup(rs);
|
||||
ImportScopeDto importScope = importScope(rs);
|
||||
String eventFamily = firstNonBlank(rs.getString("event_family"), rs.getString("event_domain"));
|
||||
LocalDate businessDate = rs.getObject("business_date", LocalDate.class);
|
||||
String externalPackageId = firstNonBlank(rs.getString("external_package_id"), rs.getString("data_package_id"));
|
||||
return new EventHubPackageRequest(
|
||||
rs.getString("tenant_key"),
|
||||
eventSource,
|
||||
sourceGroup,
|
||||
importScope,
|
||||
eventFamily,
|
||||
businessDate,
|
||||
externalPackageId
|
||||
);
|
||||
}
|
||||
|
||||
private SourceGroupRefDto sourceGroup(ResultSet rs) throws SQLException {
|
||||
String groupType = rs.getString("source_group_type");
|
||||
String sourceEntityId = rs.getString("source_group_entity_id");
|
||||
String code = rs.getString("source_group_code");
|
||||
String name = rs.getString("source_group_name");
|
||||
if (groupType == null && sourceEntityId == null && code == null && name == null) {
|
||||
return null;
|
||||
}
|
||||
return new SourceGroupRefDto(
|
||||
enumValue(SourceGroupType.class, groupType, null),
|
||||
sourceEntityId,
|
||||
code,
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
private ImportScopeDto importScope(ResultSet rs) throws SQLException {
|
||||
SourceGroupRefDto rootOrganisation = null;
|
||||
String rootEntityId = rs.getString("root_source_org_entity_id");
|
||||
String rootCode = rs.getString("root_source_org_code");
|
||||
String rootName = rs.getString("root_source_org_name");
|
||||
if (rootEntityId != null || rootCode != null || rootName != null) {
|
||||
rootOrganisation = new SourceGroupRefDto(SourceGroupType.ORGANISATION, rootEntityId, rootCode, rootName);
|
||||
}
|
||||
return new ImportScopeDto(
|
||||
enumValue(ImportScopeType.class, rs.getString("import_scope_type"), ImportScopeType.TENANT_ALL),
|
||||
rootOrganisation,
|
||||
rs.getBoolean("include_children"),
|
||||
rs.getObject("package_occurred_from", OffsetDateTime.class),
|
||||
rs.getObject("package_occurred_to", OffsetDateTime.class)
|
||||
);
|
||||
}
|
||||
|
||||
private GeoPointDto point(ResultSet rs) throws SQLException {
|
||||
BigDecimal latitude = rs.getBigDecimal("latitude");
|
||||
BigDecimal longitude = rs.getBigDecimal("longitude");
|
||||
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
|
||||
}
|
||||
|
||||
private UUID uuid(ResultSet rs, String column) throws SQLException {
|
||||
return (UUID) rs.getObject(column);
|
||||
}
|
||||
|
||||
private Long longValue(ResultSet rs, String column) throws SQLException {
|
||||
Object value = rs.getObject(column);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
|
||||
private JsonNode json(String value) {
|
||||
try {
|
||||
return value == null || value.isBlank()
|
||||
? objectMapper.createObjectNode()
|
||||
: objectMapper.readTree(value);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to parse JSON column.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String syntheticId(String prefix, UUID id) {
|
||||
return id == null ? null : prefix + ":" + id;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second) {
|
||||
if (first != null && !first.isBlank()) {
|
||||
return first;
|
||||
}
|
||||
return second != null && !second.isBlank() ? second : null;
|
||||
}
|
||||
|
||||
private <T extends Enum<T>> T enumValue(Class<T> type, String value, T fallback) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
String normalized = value.trim().toUpperCase(Locale.ROOT).replace('-', '_').replace(' ', '_');
|
||||
try {
|
||||
return Enum.valueOf(type, normalized);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedDriverEventsRequest(
|
||||
UnifiedEventSourceFamily sourceFamily,
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
String tenantKey,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
String vehicleSourceEntityId,
|
||||
String vin,
|
||||
String registrationNation,
|
||||
String registrationNumber,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo
|
||||
) {
|
||||
public UnifiedDriverEventsRequest {
|
||||
Objects.requireNonNull(sourceFamily, "sourceFamily must not be null");
|
||||
driverKey = normalize(driverKey);
|
||||
tenantKey = normalize(tenantKey);
|
||||
driverSourceEntityId = normalize(driverSourceEntityId);
|
||||
driverCardNation = normalizeUpper(driverCardNation);
|
||||
driverCardNumber = normalize(driverCardNumber);
|
||||
vehicleSourceEntityId = normalize(vehicleSourceEntityId);
|
||||
vin = normalizeUpper(vin);
|
||||
registrationNation = normalizeUpper(registrationNation);
|
||||
registrationNumber = normalize(registrationNumber);
|
||||
if (occurredFrom != null && occurredTo != null && occurredTo.isBefore(occurredFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom");
|
||||
}
|
||||
if (sourceFamily == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION) {
|
||||
Objects.requireNonNull(sessionId, "sessionId must not be null");
|
||||
if (driverKey == null) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank");
|
||||
}
|
||||
} else {
|
||||
if (tenantKey == null) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
}
|
||||
if (!hasDriverSelector(driverSourceEntityId, driverCardNumber)
|
||||
&& !hasVehicleSelector(vehicleSourceEntityId, vin, registrationNumber)) {
|
||||
throw new IllegalArgumentException("At least one driver or vehicle selector must be provided.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static UnifiedDriverEventsRequest forTachographFileSession(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo
|
||||
) {
|
||||
return new UnifiedDriverEventsRequest(
|
||||
UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION,
|
||||
sessionId,
|
||||
driverKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedDriverEventsRequest forTachographDbDriver(
|
||||
String tenantKey,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo
|
||||
) {
|
||||
return new UnifiedDriverEventsRequest(
|
||||
UnifiedEventSourceFamily.TACHOGRAPH_DB,
|
||||
null,
|
||||
null,
|
||||
tenantKey,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedDriverEventsRequest forYellowFoxDbVehicle(
|
||||
String tenantKey,
|
||||
String vehicleSourceEntityId,
|
||||
String vin,
|
||||
String registrationNation,
|
||||
String registrationNumber,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo
|
||||
) {
|
||||
return new UnifiedDriverEventsRequest(
|
||||
UnifiedEventSourceFamily.YELLOWFOX_DB,
|
||||
null,
|
||||
null,
|
||||
tenantKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
vehicleSourceEntityId,
|
||||
vin,
|
||||
registrationNation,
|
||||
registrationNumber,
|
||||
occurredFrom,
|
||||
occurredTo
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasDriverSelector() {
|
||||
return hasDriverSelector(driverSourceEntityId, driverCardNumber);
|
||||
}
|
||||
|
||||
public boolean hasVehicleSelector() {
|
||||
return hasVehicleSelector(vehicleSourceEntityId, vin, registrationNumber);
|
||||
}
|
||||
|
||||
private static boolean hasDriverSelector(String driverSourceEntityId, String driverCardNumber) {
|
||||
return driverSourceEntityId != null || driverCardNumber != null;
|
||||
}
|
||||
|
||||
private static boolean hasVehicleSelector(String vehicleSourceEntityId, String vin, String registrationNumber) {
|
||||
return vehicleSourceEntityId != null || vin != null || registrationNumber != null;
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
private static String normalizeUpper(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedDriverTimelineRequest(
|
||||
UnifiedEventSourceFamily sourceFamily,
|
||||
UUID sessionId,
|
||||
String driverKey
|
||||
) {
|
||||
public UnifiedDriverTimelineRequest {
|
||||
Objects.requireNonNull(sourceFamily, "sourceFamily must not be null");
|
||||
Objects.requireNonNull(sessionId, "sessionId must not be null");
|
||||
if (driverKey == null || driverKey.isBlank()) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank");
|
||||
}
|
||||
driverKey = driverKey.trim();
|
||||
}
|
||||
|
||||
public static UnifiedDriverTimelineRequest forTachographFileSession(
|
||||
UUID sessionId,
|
||||
String driverKey
|
||||
) {
|
||||
return new UnifiedDriverTimelineRequest(
|
||||
UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION,
|
||||
sessionId,
|
||||
driverKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
public enum UnifiedEventSourceFamily {
|
||||
TACHOGRAPH_FILE_SESSION,
|
||||
TACHOGRAPH_DB,
|
||||
YELLOWFOX_DB
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.persistence.EventHubEventReadRepository;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographDbUnifiedDriverEventSource implements UnifiedDriverEventSource {
|
||||
|
||||
private static final List<String> SOURCE_KINDS = List.of("DRIVER_CARD", "VEHICLE_UNIT");
|
||||
|
||||
private final EventHubEventReadRepository repository;
|
||||
|
||||
public TachographDbUnifiedDriverEventSource(EventHubEventReadRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(UnifiedDriverEventsRequest request) {
|
||||
return request.sourceFamily() == UnifiedEventSourceFamily.TACHOGRAPH_DB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
return repository.findEvents(request, "TACHOGRAPH", SOURCE_KINDS);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineEventBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographFileSessionUnifiedDriverEventSource implements UnifiedDriverEventSource {
|
||||
|
||||
private final TachographFileSessionRepository repository;
|
||||
private final DriverTimelineEventBuilder eventBuilder;
|
||||
|
||||
public TachographFileSessionUnifiedDriverEventSource(
|
||||
TachographFileSessionRepository repository,
|
||||
DriverTimelineEventBuilder eventBuilder
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.eventBuilder = eventBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(UnifiedDriverEventsRequest request) {
|
||||
return request.sourceFamily() == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
TachographFileSession session = repository.find(request.sessionId())
|
||||
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
|
||||
DriverExtractionSession driver = session.driversByKey().get(request.driverKey());
|
||||
if (driver == null) {
|
||||
throw new DriverNotFoundInSessionException(request.sessionId(), request.driverKey());
|
||||
}
|
||||
return eventBuilder.buildEvents(session, driver).stream()
|
||||
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean withinWindow(
|
||||
OffsetDateTime occurredAt,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo
|
||||
) {
|
||||
if (occurredAt == null) {
|
||||
return false;
|
||||
}
|
||||
if (occurredFrom != null && occurredAt.isBefore(occurredFrom)) {
|
||||
return false;
|
||||
}
|
||||
return occurredTo == null || !occurredAt.isAfter(occurredTo);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverTimelineRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
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.service.DriverNotFoundInSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.EventBackedDriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographFileSessionUnifiedDriverTimelineSource implements UnifiedDriverTimelineSource {
|
||||
|
||||
private final TachographFileSessionRepository repository;
|
||||
private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder;
|
||||
|
||||
public TachographFileSessionUnifiedDriverTimelineSource(
|
||||
TachographFileSessionRepository repository,
|
||||
EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(UnifiedDriverTimelineRequest request) {
|
||||
return request.sourceFamily() == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedDriverTimeline loadDriverTimeline(UnifiedDriverTimelineRequest request) {
|
||||
TachographFileSession session = repository.find(request.sessionId())
|
||||
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
|
||||
DriverExtractionSession driver = session.driversByKey().get(request.driverKey());
|
||||
if (driver == null) {
|
||||
throw new DriverNotFoundInSessionException(request.sessionId(), request.driverKey());
|
||||
}
|
||||
return eventBackedDriverTimelineBuilder.build(session, driver);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import java.util.List;
|
||||
|
||||
public interface UnifiedDriverEventSource {
|
||||
|
||||
boolean supports(UnifiedDriverEventsRequest request);
|
||||
|
||||
List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UnifiedDriverEventSourceService {
|
||||
|
||||
private final List<UnifiedDriverEventSource> eventSources;
|
||||
|
||||
public UnifiedDriverEventSourceService(List<UnifiedDriverEventSource> eventSources) {
|
||||
this.eventSources = List.copyOf(eventSources);
|
||||
}
|
||||
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
return eventSources.stream()
|
||||
.filter(source -> source.supports(request))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> unsupportedSource(request.sourceFamily().name()))
|
||||
.loadDriverEvents(request);
|
||||
}
|
||||
|
||||
private IllegalArgumentException unsupportedSource(String sourceFamily) {
|
||||
return new IllegalArgumentException("No unified driver event source is registered for source family " + sourceFamily + ".");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverTimelineRequest;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UnifiedDriverTimelineService {
|
||||
|
||||
private final List<UnifiedDriverTimelineSource> timelineSources;
|
||||
|
||||
public UnifiedDriverTimelineService(List<UnifiedDriverTimelineSource> timelineSources) {
|
||||
this.timelineSources = List.copyOf(timelineSources);
|
||||
}
|
||||
|
||||
public ResolvedDriverTimeline loadDriverTimeline(UnifiedDriverTimelineRequest request) {
|
||||
return timelineSources.stream()
|
||||
.filter(source -> source.supports(request))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> unsupportedSource(request.sourceFamily().name()))
|
||||
.loadDriverTimeline(request);
|
||||
}
|
||||
|
||||
private IllegalArgumentException unsupportedSource(String sourceFamily) {
|
||||
return new IllegalArgumentException("No unified driver timeline source is registered for source family " + sourceFamily + ".");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverTimelineRequest;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
|
||||
public interface UnifiedDriverTimelineSource {
|
||||
|
||||
boolean supports(UnifiedDriverTimelineRequest request);
|
||||
|
||||
ResolvedDriverTimeline loadDriverTimeline(UnifiedDriverTimelineRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.persistence.EventHubEventReadRepository;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class YellowFoxDbUnifiedDriverEventSource implements UnifiedDriverEventSource {
|
||||
|
||||
private final EventHubEventReadRepository repository;
|
||||
|
||||
public YellowFoxDbUnifiedDriverEventSource(EventHubEventReadRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(UnifiedDriverEventsRequest request) {
|
||||
return request.sourceFamily() == UnifiedEventSourceFamily.YELLOWFOX_DB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
return repository.findEvents(request, "YELLOWFOX", List.of("TELEMATICS_PLATFORM"));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,16 +8,22 @@ public record ExtractedSupportEvent(
|
|||
OffsetDateTime occurredAt,
|
||||
String eventDomain,
|
||||
String eventType,
|
||||
String eventLifecycle,
|
||||
String slot,
|
||||
String registrationKey,
|
||||
String vehicleKey,
|
||||
String country,
|
||||
String region,
|
||||
String countryFrom,
|
||||
String countryTo,
|
||||
String operation,
|
||||
BigDecimal latitude,
|
||||
BigDecimal longitude,
|
||||
String authenticationStatus,
|
||||
Long odometerKm,
|
||||
String code,
|
||||
BigDecimal avgSpeedKmh,
|
||||
BigDecimal maxSpeedKmh,
|
||||
String rawRecordPath
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInter
|
|||
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.ExtractionWarning;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
|
@ -71,6 +73,7 @@ public class DriverCardXmlExtractionService {
|
|||
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
|
||||
List<ExtractedCardActivityInterval> activityIntervals =
|
||||
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
|
||||
List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, vehicleUsageIntervals, warnings);
|
||||
|
||||
DriverExtractionSession driverSession = new DriverExtractionSession(
|
||||
driverKey,
|
||||
|
|
@ -80,7 +83,7 @@ public class DriverCardXmlExtractionService {
|
|||
List.copyOf(vehiclesByKey.values()),
|
||||
List.copyOf(vehicleUsageIntervals),
|
||||
List.copyOf(activityIntervals),
|
||||
List.of(),
|
||||
List.copyOf(supportEvents),
|
||||
List.copyOf(warnings)
|
||||
);
|
||||
Map<String, DriverExtractionSession> driversByKey = Map.of(driverKey, driverSession);
|
||||
|
|
@ -302,6 +305,284 @@ public class DriverCardXmlExtractionService {
|
|||
return result;
|
||||
}
|
||||
|
||||
private List<ExtractedSupportEvent> extractSupportEvents(
|
||||
Document document,
|
||||
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
VehicleUsageLookup vehicleUsageLookup = new VehicleUsageLookup(vehicleUsageIntervals);
|
||||
List<ExtractedSupportEvent> supportEvents = new ArrayList<>();
|
||||
Element root = document.getDocumentElement();
|
||||
extractCardPlaceSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
|
||||
extractCardGnssSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
|
||||
extractCardSpecificConditionSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
|
||||
extractCardBorderCrossingSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
|
||||
extractCardLoadUnloadSupportEvents(root, vehicleUsageLookup, supportEvents, warnings);
|
||||
supportEvents.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
|
||||
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
|
||||
return List.copyOf(supportEvents);
|
||||
}
|
||||
|
||||
private void extractCardPlaceSupportEvents(
|
||||
Element root,
|
||||
VehicleUsageLookup vehicleUsageLookup,
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
List<Element> sections = children(root, "Places");
|
||||
for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
|
||||
Element cardPlaceDailyWorkPeriod = child(sections.get(sectionIndex), "cardPlaceDailyWorkPeriod");
|
||||
List<Element> records = children(cardPlaceDailyWorkPeriod, "placeRecords");
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Element record = records.get(i);
|
||||
String path = "/DriverCard/Places[" + (sectionIndex + 1) + "]/cardPlaceDailyWorkPeriod/placeRecords[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(childText(record, "entryTime"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("CARD_PLACE_MISSING_TIME", "Driver-card place record is missing entryTime.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
|
||||
Element gnss = child(record, "entryGnssPlaceRecord");
|
||||
Element geoCoordinates = child(gnss, "geoCoordinates");
|
||||
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
|
||||
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
|
||||
String entryType = childText(record, "entryTypeDailyWorkPeriod");
|
||||
supportEvents.add(new ExtractedSupportEvent(
|
||||
"CARDPLACE-" + (i + 1),
|
||||
occurredAt,
|
||||
"PLACE",
|
||||
mapPlaceEntryType(entryType),
|
||||
null,
|
||||
null,
|
||||
usage == null ? null : usage.registrationKey(),
|
||||
usage == null ? null : usage.vehicleKey(),
|
||||
childText(record, "dailyWorkPeriodCountry"),
|
||||
childText(record, "dailyWorkPeriodRegion"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(childText(record, "vehicleOdometerValue")),
|
||||
normalizeToken(entryType),
|
||||
null,
|
||||
null,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCardGnssSupportEvents(
|
||||
Element root,
|
||||
VehicleUsageLookup vehicleUsageLookup,
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
List<Element> sections = children(root, "GnssPlaces");
|
||||
for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
|
||||
Element gnssAccumulatedDriving = child(sections.get(sectionIndex), "gnssAccumulatedDriving");
|
||||
List<Element> records = children(gnssAccumulatedDriving, "gnssAccumulatedDrivingRecord");
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Element record = records.get(i);
|
||||
String path = "/DriverCard/GnssPlaces[" + (sectionIndex + 1) + "]/gnssAccumulatedDriving/gnssAccumulatedDrivingRecord[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(childText(record, "timeStamp"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("CARD_GNSS_MISSING_TIME", "Driver-card GNSS record is missing timeStamp.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
|
||||
Element gnss = child(record, "gnssPlaceRecord");
|
||||
Element geoCoordinates = child(gnss, "geoCoordinates");
|
||||
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
|
||||
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
|
||||
supportEvents.add(new ExtractedSupportEvent(
|
||||
"CARDGNSS-" + (i + 1),
|
||||
occurredAt,
|
||||
"POSITION",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
null,
|
||||
usage == null ? null : usage.registrationKey(),
|
||||
usage == null ? null : usage.vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(childText(record, "vehicleOdometerValue")),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCardSpecificConditionSupportEvents(
|
||||
Element root,
|
||||
VehicleUsageLookup vehicleUsageLookup,
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
List<Element> sections = children(root, "SpecificConditions");
|
||||
for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
|
||||
Element specificConditionData = child(sections.get(sectionIndex), "specificConditionData");
|
||||
List<Element> records = children(specificConditionData, "specificConditionRecord");
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Element record = records.get(i);
|
||||
String path = "/DriverCard/SpecificConditions[" + (sectionIndex + 1) + "]/specificConditionData/specificConditionRecord[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(childText(record, "entryTime"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("CARD_SPECIFIC_CONDITION_MISSING_TIME", "Driver-card specific-condition record is missing entryTime.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
|
||||
String conditionCode = normalizeToken(childText(record, "specificConditionType"));
|
||||
String[] specificCondition = mapSpecificCondition(conditionCode);
|
||||
supportEvents.add(new ExtractedSupportEvent(
|
||||
"CARDSC-" + (i + 1),
|
||||
occurredAt,
|
||||
"SPECIFIC_CONDITION",
|
||||
specificCondition[0],
|
||||
specificCondition[1],
|
||||
null,
|
||||
usage == null ? null : usage.registrationKey(),
|
||||
usage == null ? null : usage.vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
conditionCode,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCardBorderCrossingSupportEvents(
|
||||
Element root,
|
||||
VehicleUsageLookup vehicleUsageLookup,
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
List<Element> sections = children(root, "BorderCrossings");
|
||||
for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
|
||||
Element cardBorderCrossings = child(sections.get(sectionIndex), "cardBorderCrossings");
|
||||
List<Element> records = children(cardBorderCrossings, "cardBorderCrossingRecord");
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Element record = records.get(i);
|
||||
String path = "/DriverCard/BorderCrossings[" + (sectionIndex + 1) + "]/cardBorderCrossings/cardBorderCrossingRecord[" + (i + 1) + "]";
|
||||
Element gnss = child(record, "gnssPlaceAuthRecord");
|
||||
OffsetDateTime occurredAt = offsetDateTime(childText(gnss, "timeStamp"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("CARD_BORDER_CROSSING_MISSING_TIME", "Driver-card border-crossing record is missing gnssPlaceAuthRecord/timeStamp.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
|
||||
Element geoCoordinates = child(gnss, "geoCoordinates");
|
||||
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
|
||||
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
|
||||
supportEvents.add(new ExtractedSupportEvent(
|
||||
"CARDBORDER-" + (i + 1),
|
||||
occurredAt,
|
||||
"BORDER_CROSSING",
|
||||
"BORDER_OUTBOUND",
|
||||
"OUTBOUND",
|
||||
null,
|
||||
usage == null ? null : usage.registrationKey(),
|
||||
usage == null ? null : usage.vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
childText(record, "countryLeft"),
|
||||
childText(record, "countryEntered"),
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(childText(record, "vehicleOdometerValue")),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCardLoadUnloadSupportEvents(
|
||||
Element root,
|
||||
VehicleUsageLookup vehicleUsageLookup,
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
List<Element> sections = children(root, "LoadUnloadOperations");
|
||||
for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
|
||||
Element cardLoadUnloadOperations = child(sections.get(sectionIndex), "cardLoadUnloadOperations");
|
||||
List<Element> records = children(cardLoadUnloadOperations, "cardLoadUnloadRecord");
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
Element record = records.get(i);
|
||||
String path = "/DriverCard/LoadUnloadOperations[" + (sectionIndex + 1) + "]/cardLoadUnloadOperations/cardLoadUnloadRecord[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(childText(record, "timeStamp"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("CARD_LOAD_UNLOAD_MISSING_TIME", "Driver-card load/unload record is missing timeStamp.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedCardVehicleUsageInterval usage = vehicleUsageLookup.resolve(occurredAt);
|
||||
Element gnss = child(record, "gnssPlaceAuthRecord");
|
||||
Element geoCoordinates = child(gnss, "geoCoordinates");
|
||||
BigDecimal latitude = geoCoordinate(geoCoordinates, "latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(geoCoordinates, "longitude", false);
|
||||
String authenticationStatus = normalizeToken(childText(gnss, "authenticationStatus"));
|
||||
String operation = mapOperation(childText(record, "operationType"));
|
||||
supportEvents.add(new ExtractedSupportEvent(
|
||||
"CARDLOAD-" + (i + 1),
|
||||
occurredAt,
|
||||
"LOAD_UNLOAD",
|
||||
operation,
|
||||
"SNAPSHOT",
|
||||
null,
|
||||
usage == null ? null : usage.registrationKey(),
|
||||
usage == null ? null : usage.vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
operation,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(childText(record, "vehicleOdometerValue")),
|
||||
normalizeToken(childText(record, "operationType")),
|
||||
null,
|
||||
null,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ExtractedCardActivityInterval> splitByVehicleCoverage(
|
||||
ExtractedCardActivityInterval interval,
|
||||
List<ExtractedCardVehicleUsageInterval> overlappingUsages
|
||||
|
|
@ -366,6 +647,26 @@ public class DriverCardXmlExtractionService {
|
|||
return usage.to().plusSeconds(1);
|
||||
}
|
||||
|
||||
private BigDecimal geoCoordinate(Element geoCoordinates, String component, boolean latitude) {
|
||||
String value = childText(geoCoordinates, component);
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal raw = new BigDecimal(value);
|
||||
BigDecimal abs = raw.abs();
|
||||
BigDecimal degreeThreshold = BigDecimal.valueOf(latitude ? 90D : 180D);
|
||||
if (abs.compareTo(degreeThreshold) <= 0) {
|
||||
return raw;
|
||||
}
|
||||
BigDecimal sign = raw.signum() < 0 ? BigDecimal.valueOf(-1L) : BigDecimal.ONE;
|
||||
BigDecimal absolute = raw.abs();
|
||||
BigDecimal[] degreeAndRemainder = absolute.divideAndRemainder(BigDecimal.valueOf(1000L));
|
||||
BigDecimal degrees = degreeAndRemainder[0];
|
||||
BigDecimal minutes = degreeAndRemainder[1].divide(BigDecimal.TEN);
|
||||
BigDecimal decimalDegrees = degrees.add(minutes.divide(BigDecimal.valueOf(60L), 8, java.math.RoundingMode.HALF_UP));
|
||||
return decimalDegrees.multiply(sign);
|
||||
}
|
||||
|
||||
private Element child(Element parent, String name) {
|
||||
if (parent == null) {
|
||||
return null;
|
||||
|
|
@ -456,6 +757,50 @@ public class DriverCardXmlExtractionService {
|
|||
return value.trim().toUpperCase().replace('-', '_').replace(' ', '_');
|
||||
}
|
||||
|
||||
private String mapPlaceEntryType(String entryType) {
|
||||
String normalized = normalizeToken(entryType);
|
||||
if (normalized == null) {
|
||||
return "DAILY_WORK_PERIOD_PLACE";
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "0" -> "BEGIN_DAILY_WORK_PERIOD";
|
||||
case "1" -> "END_DAILY_WORK_PERIOD";
|
||||
case "2" -> "BEGIN_MANUAL_DAILY_WORK_PERIOD";
|
||||
case "3" -> "END_MANUAL_DAILY_WORK_PERIOD";
|
||||
case "4" -> "BEGIN_ASSUMED_DAILY_WORK_PERIOD";
|
||||
case "5" -> "END_ASSUMED_DAILY_WORK_PERIOD";
|
||||
case "6" -> "BEGIN_GNSS_DAILY_WORK_PERIOD";
|
||||
case "7" -> "END_GNSS_DAILY_WORK_PERIOD";
|
||||
default -> "DAILY_WORK_PERIOD_PLACE";
|
||||
};
|
||||
}
|
||||
|
||||
private String[] mapSpecificCondition(String conditionCode) {
|
||||
String normalized = normalizeToken(conditionCode);
|
||||
if (normalized == null) {
|
||||
return new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "1" -> new String[]{"OUT", "BEGIN"};
|
||||
case "2" -> new String[]{"OUT", "END"};
|
||||
case "3" -> new String[]{"FERRY_TRAIN", "BEGIN"};
|
||||
case "4" -> new String[]{"FERRY_TRAIN", "END"};
|
||||
default -> new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
|
||||
};
|
||||
}
|
||||
|
||||
private String mapOperation(String operationType) {
|
||||
String normalized = normalizeToken(operationType);
|
||||
if (normalized == null) {
|
||||
return "LOAD_UNLOAD";
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "1", "LOAD" -> "LOAD";
|
||||
case "2", "UNLOAD" -> "UNLOAD";
|
||||
default -> "LOAD_UNLOAD";
|
||||
};
|
||||
}
|
||||
|
||||
private record ActivityChange(
|
||||
OffsetDateTime from,
|
||||
String activityType,
|
||||
|
|
@ -484,4 +829,42 @@ public class DriverCardXmlExtractionService {
|
|||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private final class VehicleUsageLookup {
|
||||
private final List<ExtractedCardVehicleUsageInterval> intervals;
|
||||
|
||||
private VehicleUsageLookup(List<ExtractedCardVehicleUsageInterval> intervals) {
|
||||
this.intervals = intervals == null ? List.of() : intervals;
|
||||
}
|
||||
|
||||
private ExtractedCardVehicleUsageInterval resolve(OffsetDateTime occurredAt) {
|
||||
if (occurredAt == null || intervals.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int low = 0;
|
||||
int high = intervals.size() - 1;
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
ExtractedCardVehicleUsageInterval interval = intervals.get(mid);
|
||||
if (interval.from().isAfter(occurredAt)) {
|
||||
high = mid - 1;
|
||||
continue;
|
||||
}
|
||||
if (covers(interval, occurredAt)) {
|
||||
return interval;
|
||||
}
|
||||
low = mid + 1;
|
||||
}
|
||||
for (int i = Math.min(high, intervals.size() - 1); i >= 0; i--) {
|
||||
ExtractedCardVehicleUsageInterval interval = intervals.get(i);
|
||||
if (covers(interval, occurredAt)) {
|
||||
return interval;
|
||||
}
|
||||
if (interval.from().isBefore(occurredAt) && usageEndExclusive(interval, null).isBefore(occurredAt)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,448 @@
|
|||
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.ExtractedSupportEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||
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 com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class EventBackedDriverTimelineBuilder {
|
||||
|
||||
private final DriverTimelineEventBuilder eventBuilder;
|
||||
|
||||
public EventBackedDriverTimelineBuilder(DriverTimelineEventBuilder eventBuilder) {
|
||||
this.eventBuilder = eventBuilder;
|
||||
}
|
||||
|
||||
public ResolvedDriverTimeline build(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession
|
||||
) {
|
||||
TachographTimelineEventBundle bundle = eventBuilder.buildEventBundle(session, driverSession);
|
||||
List<ResolvedActivityInterval> activityIntervals =
|
||||
reconstructActivityIntervals(bundle.activityEvents());
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals =
|
||||
reconstructVehicleUsageIntervals(session.sessionId(), driverSession.driverKey(), bundle.vehicleUsageEvents());
|
||||
List<ExtractedSupportEvent> supportEvents =
|
||||
reconstructSupportEvents(bundle.supportEvents());
|
||||
List<ExtractionWarning> warnings = mergeWarnings(session.warnings(), driverSession.warnings());
|
||||
|
||||
OffsetDateTime loadedFrom = minTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
|
||||
OffsetDateTime loadedTo = maxTimestamp(activityIntervals, vehicleUsageIntervals, supportEvents);
|
||||
return new ResolvedDriverTimeline(
|
||||
resolveSourceKind(session, bundle),
|
||||
loadedFrom,
|
||||
loadedTo,
|
||||
vehicleUsageIntervals,
|
||||
activityIntervals,
|
||||
supportEvents,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private List<ResolvedActivityInterval> reconstructActivityIntervals(List<EventHubEventDto> activityEvents) {
|
||||
if (activityEvents == null || activityEvents.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, ActivityAccumulator> byIntervalId = new LinkedHashMap<>();
|
||||
for (EventHubEventDto event : activityEvents) {
|
||||
if (!"DRIVER_ACTIVITY".equals(event.eventDomain().name())) {
|
||||
continue;
|
||||
}
|
||||
JsonNode raw = raw(event);
|
||||
String intervalId = text(raw, "intervalId");
|
||||
if (intervalId == null) {
|
||||
intervalId = text(raw, "sourceRowId");
|
||||
}
|
||||
if (intervalId == null) {
|
||||
intervalId = event.externalSourceEventId();
|
||||
}
|
||||
String resolvedIntervalId = intervalId;
|
||||
ActivityAccumulator accumulator = byIntervalId.computeIfAbsent(
|
||||
resolvedIntervalId,
|
||||
ignored -> new ActivityAccumulator(resolvedIntervalId)
|
||||
);
|
||||
accumulator.accept(event, raw);
|
||||
}
|
||||
List<ResolvedActivityInterval> result = new ArrayList<>(byIntervalId.size());
|
||||
for (ActivityAccumulator accumulator : byIntervalId.values()) {
|
||||
ResolvedActivityInterval interval = accumulator.finish();
|
||||
if (interval != null) {
|
||||
result.add(interval);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(ResolvedActivityInterval::from)
|
||||
.thenComparing(ResolvedActivityInterval::to)
|
||||
.thenComparing(ResolvedActivityInterval::activityType, Comparator.nullsLast(String::compareTo)));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> reconstructVehicleUsageIntervals(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
List<EventHubEventDto> vehicleUsageEvents
|
||||
) {
|
||||
if (vehicleUsageEvents == null || vehicleUsageEvents.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, VehicleUsageAccumulator> byIntervalId = new LinkedHashMap<>();
|
||||
for (EventHubEventDto event : vehicleUsageEvents) {
|
||||
if (!"DRIVER_CARD".equals(event.eventDomain().name())) {
|
||||
continue;
|
||||
}
|
||||
JsonNode raw = raw(event);
|
||||
String intervalId = text(raw, "intervalId");
|
||||
if (intervalId == null) {
|
||||
intervalId = text(raw, "sourceRowId");
|
||||
}
|
||||
if (intervalId == null) {
|
||||
intervalId = event.externalSourceEventId();
|
||||
}
|
||||
String resolvedIntervalId = intervalId;
|
||||
VehicleUsageAccumulator accumulator = byIntervalId.computeIfAbsent(
|
||||
resolvedIntervalId,
|
||||
ignored -> new VehicleUsageAccumulator(sessionId, driverKey, resolvedIntervalId)
|
||||
);
|
||||
accumulator.accept(event, raw);
|
||||
}
|
||||
List<ResolvedVehicleUsageInterval> result = new ArrayList<>(byIntervalId.size());
|
||||
for (VehicleUsageAccumulator accumulator : byIntervalId.values()) {
|
||||
ResolvedVehicleUsageInterval interval = accumulator.finish();
|
||||
if (interval != null) {
|
||||
result.add(interval);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(ResolvedVehicleUsageInterval::from)
|
||||
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<ExtractedSupportEvent> reconstructSupportEvents(List<EventHubEventDto> supportEvents) {
|
||||
if (supportEvents == null || supportEvents.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ExtractedSupportEvent> result = new ArrayList<>(supportEvents.size());
|
||||
for (EventHubEventDto event : supportEvents) {
|
||||
JsonNode raw = raw(event);
|
||||
String eventId = text(raw, "supportEventId");
|
||||
if (eventId == null) {
|
||||
eventId = event.externalSourceEventId();
|
||||
}
|
||||
BigDecimal latitude = event.position() == null ? null : event.position().latitude();
|
||||
BigDecimal longitude = event.position() == null ? null : event.position().longitude();
|
||||
result.add(new ExtractedSupportEvent(
|
||||
eventId,
|
||||
event.occurredAt(),
|
||||
event.eventDomain().name(),
|
||||
text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),
|
||||
event.lifecycle().name(),
|
||||
text(raw, "slot"),
|
||||
text(raw, "registrationKey"),
|
||||
text(raw, "vehicleKey"),
|
||||
text(raw, "country"),
|
||||
text(raw, "region"),
|
||||
text(raw, "countryFrom"),
|
||||
text(raw, "countryTo"),
|
||||
text(raw, "operation"),
|
||||
latitude,
|
||||
longitude,
|
||||
text(raw, "authenticationStatus"),
|
||||
longValue(raw, "odometerKm"),
|
||||
text(raw, "code"),
|
||||
decimal(raw, "avgSpeedKmh"),
|
||||
decimal(raw, "maxSpeedKmh"),
|
||||
text(raw, "rawRecordPath")
|
||||
));
|
||||
}
|
||||
result.sort(Comparator.comparing(ExtractedSupportEvent::occurredAt)
|
||||
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<ExtractionWarning> mergeWarnings(List<ExtractionWarning> sessionWarnings, List<ExtractionWarning> driverWarnings) {
|
||||
LinkedHashSet<ExtractionWarning> merged = new LinkedHashSet<>();
|
||||
if (sessionWarnings != null) {
|
||||
merged.addAll(sessionWarnings);
|
||||
}
|
||||
if (driverWarnings != null) {
|
||||
merged.addAll(driverWarnings);
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private OffsetDateTime minTimestamp(
|
||||
List<ResolvedActivityInterval> activityIntervals,
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||
List<ExtractedSupportEvent> supportEvents
|
||||
) {
|
||||
OffsetDateTime min = null;
|
||||
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||
min = min(min, interval.from());
|
||||
}
|
||||
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||
min = min(min, interval.from());
|
||||
}
|
||||
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||
min = min(min, supportEvent.occurredAt());
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
private OffsetDateTime maxTimestamp(
|
||||
List<ResolvedActivityInterval> activityIntervals,
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||
List<ExtractedSupportEvent> supportEvents
|
||||
) {
|
||||
OffsetDateTime max = null;
|
||||
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||
max = max(max, interval.to());
|
||||
}
|
||||
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||
max = max(max, interval.to());
|
||||
}
|
||||
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||
max = max(max, supportEvent.occurredAt());
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
private String resolveSourceKind(TachographFileSession session, TachographTimelineEventBundle bundle) {
|
||||
for (EventHubEventDto event : bundle.allEvents()) {
|
||||
JsonNode raw = raw(event);
|
||||
String sourceKind = text(raw, "sourceKind");
|
||||
if (sourceKind != null) {
|
||||
return sourceKind;
|
||||
}
|
||||
if (event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||
&& event.packageInfo().eventSource().sourceKind() != null) {
|
||||
return event.packageInfo().eventSource().sourceKind();
|
||||
}
|
||||
}
|
||||
return session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
|
||||
}
|
||||
|
||||
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isBefore(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
|
||||
private JsonNode raw(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
return value == null || value.isNull() ? null : value.asText(null);
|
||||
}
|
||||
|
||||
private boolean booleanValue(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return false;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
return value != null && !value.isNull() && value.asBoolean(false);
|
||||
}
|
||||
|
||||
private Long longValue(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
|
||||
}
|
||||
|
||||
private BigDecimal decimal(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (value.isNumber()) {
|
||||
return value.decimalValue();
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : new BigDecimal(text);
|
||||
}
|
||||
|
||||
private List<String> stringList(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return List.of();
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull() || !value.isArray()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
value.forEach(item -> {
|
||||
String text = item == null || item.isNull() ? null : item.asText(null);
|
||||
if (text != null && !text.isBlank()) {
|
||||
result.add(text);
|
||||
}
|
||||
});
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private String detailText(EventHubEventDto event, String field) {
|
||||
if (event.eventDetails() == null || event.eventDetails().attributes() == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = event.eventDetails().attributes().get(field);
|
||||
return value == null || value.isNull() ? null : value.asText(null);
|
||||
}
|
||||
|
||||
private String activityType(EventHubEventDto event) {
|
||||
return switch (event.eventType()) {
|
||||
case DRIVE -> "DRIVE";
|
||||
case WORK -> "WORK";
|
||||
case AVAILABILITY -> "AVAILABILITY";
|
||||
case BREAK_REST -> "BREAK_REST";
|
||||
default -> "UNKNOWN";
|
||||
};
|
||||
}
|
||||
|
||||
private Long toKilometers(Long meters) {
|
||||
return meters == null ? null : meters / 1_000L;
|
||||
}
|
||||
|
||||
private final class ActivityAccumulator {
|
||||
private final String intervalId;
|
||||
private OffsetDateTime startedAt;
|
||||
private OffsetDateTime endedAt;
|
||||
private EventHubEventDto sample;
|
||||
private JsonNode raw;
|
||||
|
||||
private ActivityAccumulator(String intervalId) {
|
||||
this.intervalId = intervalId;
|
||||
}
|
||||
|
||||
private void accept(EventHubEventDto event, JsonNode raw) {
|
||||
if (sample == null) {
|
||||
sample = event;
|
||||
this.raw = raw;
|
||||
}
|
||||
if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.START) {
|
||||
startedAt = event.occurredAt();
|
||||
} else if (event.lifecycle() == at.procon.eventhub.dto.EventLifecycle.END) {
|
||||
endedAt = event.occurredAt();
|
||||
}
|
||||
}
|
||||
|
||||
private ResolvedActivityInterval finish() {
|
||||
if (sample == null || startedAt == null || endedAt == null || !endedAt.isAfter(startedAt)) {
|
||||
return null;
|
||||
}
|
||||
return new ResolvedActivityInterval(
|
||||
intervalId,
|
||||
startedAt,
|
||||
endedAt,
|
||||
java.time.Duration.between(startedAt, endedAt).getSeconds(),
|
||||
activityType(sample),
|
||||
detailText(sample, "cardSlot"),
|
||||
detailText(sample, "cardStatus"),
|
||||
detailText(sample, "drivingStatus"),
|
||||
text(raw, "registrationKey"),
|
||||
text(raw, "vehicleKey"),
|
||||
text(raw, "sourceKind"),
|
||||
stringList(raw, "sourceRowIds"),
|
||||
booleanValue(raw, "synthetic"),
|
||||
booleanValue(raw, "clippedToRequestedPeriod"),
|
||||
text(raw, "level") == null ? "RAW_INTERVAL" : text(raw, "level")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private final class VehicleUsageAccumulator {
|
||||
private final UUID sessionId;
|
||||
private final String driverKey;
|
||||
private final String intervalId;
|
||||
private OffsetDateTime startedAt;
|
||||
private OffsetDateTime endedAt;
|
||||
private Long odometerBeginKm;
|
||||
private Long odometerEndKm;
|
||||
private JsonNode raw;
|
||||
|
||||
private VehicleUsageAccumulator(UUID sessionId, String driverKey, String intervalId) {
|
||||
this.sessionId = sessionId;
|
||||
this.driverKey = driverKey;
|
||||
this.intervalId = intervalId;
|
||||
}
|
||||
|
||||
private void accept(EventHubEventDto event, JsonNode raw) {
|
||||
if (this.raw == null) {
|
||||
this.raw = raw;
|
||||
}
|
||||
if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_INSERTED) {
|
||||
startedAt = event.occurredAt();
|
||||
odometerBeginKm = toKilometers(event.odometerM());
|
||||
} else if (event.eventType() == at.procon.eventhub.dto.EventType.CARD_WITHDRAWN) {
|
||||
endedAt = event.occurredAt();
|
||||
odometerEndKm = toKilometers(event.odometerM());
|
||||
}
|
||||
}
|
||||
|
||||
private ResolvedVehicleUsageInterval finish() {
|
||||
if (startedAt == null) {
|
||||
return null;
|
||||
}
|
||||
return ResolvedVehicleUsageInterval.resolved(
|
||||
sessionId,
|
||||
driverKey,
|
||||
intervalId,
|
||||
startedAt,
|
||||
endedAt,
|
||||
odometerBeginKm,
|
||||
odometerEndKm,
|
||||
text(raw, "registrationKey"),
|
||||
text(raw, "vehicleKey"),
|
||||
text(raw, "sourceKind"),
|
||||
stringList(raw, "sourceRowIds")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,637 @@
|
|||
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(), supportEvent.eventLifecycle());
|
||||
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("countryFrom", supportEvent.countryFrom());
|
||||
raw.put("countryTo", supportEvent.countryTo());
|
||||
raw.put("operation", supportEvent.operation());
|
||||
raw.put("authenticationStatus", supportEvent.authenticationStatus());
|
||||
raw.put("odometerKm", supportEvent.odometerKm());
|
||||
raw.put("code", supportEvent.code());
|
||||
raw.put("avgSpeedKmh", supportEvent.avgSpeedKmh());
|
||||
raw.put("maxSpeedKmh", supportEvent.maxSpeedKmh());
|
||||
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 "BORDER_CROSSING" -> EventDomain.BORDER_CROSSING;
|
||||
case "LOAD_UNLOAD" -> EventDomain.LOAD_UNLOAD;
|
||||
case "SPECIFIC_CONDITION" -> EventDomain.SPECIFIC_CONDITION;
|
||||
case "SPEEDING" -> EventDomain.SPEEDING;
|
||||
default -> EventDomain.TELEMATICS_DATA;
|
||||
};
|
||||
}
|
||||
|
||||
private EventType supportEventType(EventDomain eventDomain, String eventType, String code) {
|
||||
EventType explicit = parseEnum(EventType.class, eventType, null);
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
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;
|
||||
}
|
||||
case BORDER_CROSSING -> EventType.BORDER_OUTBOUND;
|
||||
case LOAD_UNLOAD -> EventType.LOAD_UNLOAD;
|
||||
case SPEEDING -> EventType.SPEEDING;
|
||||
default -> parseEnum(EventType.class, eventType, EventType.UNKNOWN_EVENT);
|
||||
};
|
||||
}
|
||||
|
||||
private EventLifecycle supportEventLifecycle(EventDomain eventDomain, String eventType, String lifecycle) {
|
||||
EventLifecycle explicit = parseEnum(EventLifecycle.class, lifecycle, null);
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
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("GNSS_ACCUMULATED_DRIVING");
|
||||
case BORDER_CROSSING -> detailsFactory.borderCrossing(supportEvent.countryFrom(), supportEvent.countryTo());
|
||||
case LOAD_UNLOAD -> detailsFactory.loadUnload(supportEvent.operation());
|
||||
case SPECIFIC_CONDITION -> detailsFactory.specificCondition();
|
||||
case SPEEDING -> detailsFactory.speeding(supportEvent.avgSpeedKmh(), supportEvent.maxSpeedKmh(), null);
|
||||
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,21 @@ public class TachographFileSessionProcessingService {
|
|||
|
||||
private final TachographFileSessionRepository repository;
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public TachographFileSessionProcessingService(
|
||||
TachographFileSessionRepository repository,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +67,7 @@ public class TachographFileSessionProcessingService {
|
|||
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
||||
}
|
||||
|
||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
|
||||
ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
|
||||
OffsetDateTime loadedFrom = timeline.loadedFrom();
|
||||
OffsetDateTime loadedTo = timeline.loadedTo();
|
||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? loadedFrom : utc(effectiveRequest.occurredFrom());
|
||||
|
|
@ -142,7 +149,7 @@ public class TachographFileSessionProcessingService {
|
|||
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
||||
}
|
||||
|
||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
|
||||
ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
|
||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
|
||||
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
|
||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||
|
|
@ -161,13 +168,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 +185,7 @@ public class TachographFileSessionProcessingService {
|
|||
requestedTo
|
||||
);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
|
||||
driverTimelineBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(
|
||||
rawDrivingInterruptionIntervals,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(
|
||||
rawDailyWeeklyRestCandidateIntervals,
|
||||
|
|
@ -186,9 +193,7 @@ public class TachographFileSessionProcessingService {
|
|||
requestedTo
|
||||
);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
|
||||
driverTimelineBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(
|
||||
rawDailyWeeklyRestCandidateIntervals
|
||||
);
|
||||
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(
|
||||
rawDrivingInterruptionVehicleChangeIntervals,
|
||||
|
|
@ -196,13 +201,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
|
||||
|
|
@ -246,6 +248,17 @@ public class TachographFileSessionProcessingService {
|
|||
);
|
||||
}
|
||||
|
||||
private ResolvedDriverTimeline resolveTimeline(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driver
|
||||
) {
|
||||
if (properties.getTachographFileSession().getProcessing().getTimelineInputMode()
|
||||
== EventHubProperties.TimelineInputMode.EVENTS) {
|
||||
return eventBackedDriverTimelineBuilder.build(session, driver);
|
||||
}
|
||||
return driverTimelineBuilder.build(session, driver);
|
||||
}
|
||||
|
||||
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
|
||||
List<TachographEsperActivityIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
|
|
|
|||
|
|
@ -369,6 +369,9 @@ public class VehicleUnitXmlExtractionService {
|
|||
extractVuPlaceSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
extractVuSpecificConditionSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
extractVuBorderCrossingSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
extractVuLoadUnloadSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
extractVuSpeedingSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||
}
|
||||
|
||||
private void extractVuPlaceSupportEvents(
|
||||
|
|
@ -415,16 +418,22 @@ public class VehicleUnitXmlExtractionService {
|
|||
occurredAt,
|
||||
"PLACE",
|
||||
mapPlaceEntryType(entryType),
|
||||
null,
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
text(record, "placeRecord/dailyWorkPeriodCountry"),
|
||||
text(record, "placeRecord/dailyWorkPeriodRegion"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(text(record, "placeRecord/vehicleOdometerValue")),
|
||||
normalizeToken(entryType),
|
||||
null,
|
||||
null,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
|
|
@ -480,17 +489,23 @@ public class VehicleUnitXmlExtractionService {
|
|||
"VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||
occurredAt,
|
||||
"POSITION",
|
||||
"GNSS_ACCUMULATED_DRIVING",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(text(record, "vehicleOdometerValue")),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
|
|
@ -531,6 +546,7 @@ public class VehicleUnitXmlExtractionService {
|
|||
}
|
||||
|
||||
String conditionCode = normalizeToken(text(record, "specificConditionType"));
|
||||
String[] specificCondition = mapSpecificCondition(conditionCode);
|
||||
for (DriverAssignment assignment : assignments) {
|
||||
addSupportEvent(
|
||||
driversByKey,
|
||||
|
|
@ -539,7 +555,8 @@ public class VehicleUnitXmlExtractionService {
|
|||
"VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||
occurredAt,
|
||||
"SPECIFIC_CONDITION",
|
||||
"SPECIFIC_CONDITION",
|
||||
specificCondition[0],
|
||||
specificCondition[1],
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
|
|
@ -549,7 +566,12 @@ public class VehicleUnitXmlExtractionService {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
conditionCode,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
|
|
@ -558,6 +580,247 @@ public class VehicleUnitXmlExtractionService {
|
|||
}
|
||||
}
|
||||
|
||||
private void extractVuBorderCrossingSupportEvents(
|
||||
Document document,
|
||||
VehicleContext vehicleContext,
|
||||
List<VuCardIwInterval> vuCardIwIntervals,
|
||||
Map<String, DriverExtractionBuilder> driversByKey,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
NodeList records = nodes(document, "/VehicleUnit/Activities/vuBorderCrossingData/vuBorderCrossingRecord");
|
||||
for (int i = 0; i < records.getLength(); i++) {
|
||||
Element record = (Element) records.item(i);
|
||||
String path = "/VehicleUnit/Activities/vuBorderCrossingData/vuBorderCrossingRecord[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(text(record, "gnssPlaceAuthRecord/timeStamp"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("VU_BORDER_CROSSING_MISSING_TIME", "Vehicle-unit border-crossing record is missing gnssPlaceAuthRecord/timeStamp.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
List<DriverAssignment> assignments = new ArrayList<>();
|
||||
assignments.addAll(resolveDriverAssignments(
|
||||
occurredAt,
|
||||
driverKeyFromCardNode(record, "cardNumberDriverSlot"),
|
||||
"DRIVER",
|
||||
vuCardIwIntervals
|
||||
));
|
||||
assignments.addAll(resolveDriverAssignments(
|
||||
occurredAt,
|
||||
driverKeyFromCardNode(record, "cardNumberCodriverSlot"),
|
||||
"CO_DRIVER",
|
||||
vuCardIwIntervals
|
||||
));
|
||||
assignments = distinctAssignments(assignments);
|
||||
if (assignments.isEmpty()) {
|
||||
warnings.add(new ExtractionWarning("VU_BORDER_CROSSING_UNASSIGNED", "Vehicle-unit border-crossing record could not be assigned to an active driver session.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
BigDecimal latitude = geoCoordinate(record, "gnssPlaceAuthRecord/geoCoordinates/latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(record, "gnssPlaceAuthRecord/geoCoordinates/longitude", false);
|
||||
String authenticationStatus = normalizeToken(text(record, "gnssPlaceAuthRecord/authenticationStatus"));
|
||||
for (DriverAssignment assignment : assignments) {
|
||||
addSupportEvent(
|
||||
driversByKey,
|
||||
assignment,
|
||||
new ExtractedSupportEvent(
|
||||
"VUBORDER-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||
occurredAt,
|
||||
"BORDER_CROSSING",
|
||||
"BORDER_OUTBOUND",
|
||||
"OUTBOUND",
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
text(record, "countryLeft"),
|
||||
text(record, "countryEntered"),
|
||||
null,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(text(record, "vehicleOdometerValue")),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractVuLoadUnloadSupportEvents(
|
||||
Document document,
|
||||
VehicleContext vehicleContext,
|
||||
List<VuCardIwInterval> vuCardIwIntervals,
|
||||
Map<String, DriverExtractionBuilder> driversByKey,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
NodeList records = nodes(document, "/VehicleUnit/Activities/vuLoadUnloadData/vuLoadUnloadRecord");
|
||||
for (int i = 0; i < records.getLength(); i++) {
|
||||
Element record = (Element) records.item(i);
|
||||
String path = "/VehicleUnit/Activities/vuLoadUnloadData/vuLoadUnloadRecord[" + (i + 1) + "]";
|
||||
OffsetDateTime occurredAt = offsetDateTime(text(record, "timeStamp"));
|
||||
if (occurredAt == null) {
|
||||
warnings.add(new ExtractionWarning("VU_LOAD_UNLOAD_MISSING_TIME", "Vehicle-unit load/unload record is missing timeStamp.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
List<DriverAssignment> assignments = new ArrayList<>();
|
||||
assignments.addAll(resolveDriverAssignments(
|
||||
occurredAt,
|
||||
driverKeyFromCardNode(record, "cardNumberDriverSlot"),
|
||||
"DRIVER",
|
||||
vuCardIwIntervals
|
||||
));
|
||||
assignments.addAll(resolveDriverAssignments(
|
||||
occurredAt,
|
||||
driverKeyFromCardNode(record, "cardNumberCodriverSlot"),
|
||||
"CO_DRIVER",
|
||||
vuCardIwIntervals
|
||||
));
|
||||
assignments = distinctAssignments(assignments);
|
||||
if (assignments.isEmpty()) {
|
||||
warnings.add(new ExtractionWarning("VU_LOAD_UNLOAD_UNASSIGNED", "Vehicle-unit load/unload record could not be assigned to an active driver session.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
BigDecimal latitude = geoCoordinate(record, "gnssPlaceAuthRecord/geoCoordinates/latitude", true);
|
||||
BigDecimal longitude = geoCoordinate(record, "gnssPlaceAuthRecord/geoCoordinates/longitude", false);
|
||||
String authenticationStatus = normalizeToken(text(record, "gnssPlaceAuthRecord/authenticationStatus"));
|
||||
String operation = mapOperation(text(record, "operationType"));
|
||||
for (DriverAssignment assignment : assignments) {
|
||||
addSupportEvent(
|
||||
driversByKey,
|
||||
assignment,
|
||||
new ExtractedSupportEvent(
|
||||
"VULOAD-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||
occurredAt,
|
||||
"LOAD_UNLOAD",
|
||||
operation,
|
||||
"SNAPSHOT",
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
operation,
|
||||
latitude,
|
||||
longitude,
|
||||
authenticationStatus,
|
||||
longValue(text(record, "vehicleOdometerValue")),
|
||||
normalizeToken(text(record, "operationType")),
|
||||
null,
|
||||
null,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractVuSpeedingSupportEvents(
|
||||
Document document,
|
||||
VehicleContext vehicleContext,
|
||||
List<VuCardIwInterval> vuCardIwIntervals,
|
||||
Map<String, DriverExtractionBuilder> driversByKey,
|
||||
List<ExtractionWarning> warnings
|
||||
) {
|
||||
NodeList records = nodes(document, "/VehicleUnit/EventsFaults/vuOverSpeedingEventData/vuOverSpeedingEventRecords");
|
||||
for (int i = 0; i < records.getLength(); i++) {
|
||||
Element record = (Element) records.item(i);
|
||||
String path = "/VehicleUnit/EventsFaults/vuOverSpeedingEventData/vuOverSpeedingEventRecords[" + (i + 1) + "]";
|
||||
OffsetDateTime beginAt = offsetDateTime(text(record, "eventBeginTime"));
|
||||
OffsetDateTime endAt = offsetDateTime(text(record, "eventEndTime"));
|
||||
if (beginAt == null && endAt == null) {
|
||||
warnings.add(new ExtractionWarning("VU_SPEEDING_MISSING_TIME", "Vehicle-unit speeding record is missing both eventBeginTime and eventEndTime.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
List<DriverAssignment> assignments = distinctAssignments(resolveDriverAssignments(
|
||||
beginAt != null ? beginAt : endAt,
|
||||
driverKeyFromCardNode(record, "cardNumberDriverSlotBegin"),
|
||||
"DRIVER",
|
||||
vuCardIwIntervals
|
||||
));
|
||||
if (assignments.isEmpty()) {
|
||||
warnings.add(new ExtractionWarning("VU_SPEEDING_UNASSIGNED", "Vehicle-unit speeding record could not be assigned to an active driver session.", path));
|
||||
continue;
|
||||
}
|
||||
|
||||
BigDecimal avgSpeedKmh = decimalValue(text(record, "averageSpeedValue"));
|
||||
BigDecimal maxSpeedKmh = decimalValue(text(record, "maxSpeedValue"));
|
||||
for (DriverAssignment assignment : assignments) {
|
||||
if (beginAt != null) {
|
||||
addSupportEvent(
|
||||
driversByKey,
|
||||
assignment,
|
||||
new ExtractedSupportEvent(
|
||||
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-BEGIN",
|
||||
beginAt,
|
||||
"SPEEDING",
|
||||
"SPEEDING",
|
||||
"BEGIN",
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
normalizeToken(text(record, "eventType")),
|
||||
avgSpeedKmh,
|
||||
maxSpeedKmh,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
);
|
||||
}
|
||||
if (endAt != null) {
|
||||
addSupportEvent(
|
||||
driversByKey,
|
||||
assignment,
|
||||
new ExtractedSupportEvent(
|
||||
"VUSPEED-" + (i + 1) + "-" + assignment.driverKey() + "-END",
|
||||
endAt,
|
||||
"SPEEDING",
|
||||
"SPEEDING",
|
||||
"END",
|
||||
assignment.slot(),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
normalizeToken(text(record, "eventType")),
|
||||
avgSpeedKmh,
|
||||
maxSpeedKmh,
|
||||
path
|
||||
),
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPartiallyCovered(ExtractedCardActivityInterval interval, List<ActivitySegment> segments) {
|
||||
if (segments.isEmpty()) {
|
||||
return true;
|
||||
|
|
@ -696,6 +959,13 @@ public class VehicleUnitXmlExtractionService {
|
|||
return Long.parseLong(value.trim());
|
||||
}
|
||||
|
||||
private BigDecimal decimalValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return new BigDecimal(value.trim());
|
||||
}
|
||||
|
||||
private BigDecimal geoCoordinate(Object node, String expression, boolean latitude) {
|
||||
String value = text(node, expression);
|
||||
if (value == null) {
|
||||
|
|
@ -759,6 +1029,32 @@ public class VehicleUnitXmlExtractionService {
|
|||
};
|
||||
}
|
||||
|
||||
private String[] mapSpecificCondition(String conditionCode) {
|
||||
String normalized = normalizeToken(conditionCode);
|
||||
if (normalized == null) {
|
||||
return new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "1" -> new String[]{"OUT", "BEGIN"};
|
||||
case "2" -> new String[]{"OUT", "END"};
|
||||
case "3" -> new String[]{"FERRY_TRAIN", "BEGIN"};
|
||||
case "4" -> new String[]{"FERRY_TRAIN", "END"};
|
||||
default -> new String[]{"UNKNOWN_EVENT", "SNAPSHOT"};
|
||||
};
|
||||
}
|
||||
|
||||
private String mapOperation(String operationType) {
|
||||
String normalized = normalizeToken(operationType);
|
||||
if (normalized == null) {
|
||||
return "LOAD_UNLOAD";
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "1", "LOAD" -> "LOAD";
|
||||
case "2", "UNLOAD" -> "UNLOAD";
|
||||
default -> "LOAD_UNLOAD";
|
||||
};
|
||||
}
|
||||
|
||||
private String joinCardNumber(Element node, String basePath) {
|
||||
String driverIdentification = text(node, basePath + "/driverIdentification");
|
||||
if (driverIdentification == null) {
|
||||
|
|
|
|||
|
|
@ -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,88 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class UnifiedDriverEventsRequestTest {
|
||||
|
||||
@Test
|
||||
void buildsFileSessionRequest() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
|
||||
UnifiedDriverEventsRequest request = UnifiedDriverEventsRequest.forTachographFileSession(
|
||||
sessionId,
|
||||
" 12:123 ",
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z")
|
||||
);
|
||||
|
||||
assertThat(request.sourceFamily()).isEqualTo(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
|
||||
assertThat(request.sessionId()).isEqualTo(sessionId);
|
||||
assertThat(request.driverKey()).isEqualTo("12:123");
|
||||
assertThat(request.tenantKey()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsTachographDbDriverRequest() {
|
||||
UnifiedDriverEventsRequest request = UnifiedDriverEventsRequest.forTachographDbDriver(
|
||||
" default ",
|
||||
" DRIVER:42 ",
|
||||
"at",
|
||||
" 123 ",
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z")
|
||||
);
|
||||
|
||||
assertThat(request.sourceFamily()).isEqualTo(UnifiedEventSourceFamily.TACHOGRAPH_DB);
|
||||
assertThat(request.tenantKey()).isEqualTo("default");
|
||||
assertThat(request.driverSourceEntityId()).isEqualTo("DRIVER:42");
|
||||
assertThat(request.driverCardNation()).isEqualTo("AT");
|
||||
assertThat(request.driverCardNumber()).isEqualTo("123");
|
||||
assertThat(request.hasDriverSelector()).isTrue();
|
||||
assertThat(request.hasVehicleSelector()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsYellowFoxVehicleRequest() {
|
||||
UnifiedDriverEventsRequest request = UnifiedDriverEventsRequest.forYellowFoxDbVehicle(
|
||||
"default",
|
||||
"VEHICLE:99",
|
||||
"wdb123",
|
||||
"de",
|
||||
"W-123AB",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertThat(request.sourceFamily()).isEqualTo(UnifiedEventSourceFamily.YELLOWFOX_DB);
|
||||
assertThat(request.vehicleSourceEntityId()).isEqualTo("VEHICLE:99");
|
||||
assertThat(request.vin()).isEqualTo("WDB123");
|
||||
assertThat(request.registrationNation()).isEqualTo("DE");
|
||||
assertThat(request.registrationNumber()).isEqualTo("W-123AB");
|
||||
assertThat(request.hasVehicleSelector()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDbRequestWithoutSubjectSelector() {
|
||||
assertThatThrownBy(() -> new UnifiedDriverEventsRequest(
|
||||
UnifiedEventSourceFamily.TACHOGRAPH_DB,
|
||||
null,
|
||||
null,
|
||||
"default",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("At least one driver or vehicle selector");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.dto.EventDomain;
|
||||
import at.procon.eventhub.dto.EventLifecycle;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverEventsRequest;
|
||||
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.service.DriverKeyFactory;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.InMemoryTachographFileSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.IntervalBackedDriverTimelineEventBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.VehicleKeyFactory;
|
||||
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 UnifiedDriverEventSourceServiceTest {
|
||||
|
||||
@Test
|
||||
void loadsNormalizedFileSessionEventsThroughUnifiedService() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
DriverTimelineBuilder timelineBuilder = new DriverTimelineBuilder();
|
||||
UnifiedDriverEventSourceService service = new UnifiedDriverEventSourceService(List.of(
|
||||
new TachographFileSessionUnifiedDriverEventSource(
|
||||
repository,
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
timelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
DriverExtractionSession driver = driver();
|
||||
TachographFileSession session = session(driver);
|
||||
repository.save(session);
|
||||
|
||||
List<at.procon.eventhub.dto.EventHubEventDto> events = service.loadDriverEvents(
|
||||
UnifiedDriverEventsRequest.forTachographFileSession(
|
||||
session.sessionId(),
|
||||
driver.driverKey(),
|
||||
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T09:00:00Z")
|
||||
)
|
||||
);
|
||||
|
||||
assertThat(events).hasSize(3);
|
||||
assertThat(events).extracting(event -> event.occurredAt())
|
||||
.containsExactly(
|
||||
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T09:00:00Z")
|
||||
);
|
||||
assertThat(events).extracting(event -> event.eventDomain())
|
||||
.containsExactly(
|
||||
EventDomain.DRIVER_ACTIVITY,
|
||||
EventDomain.POSITION,
|
||||
EventDomain.DRIVER_ACTIVITY
|
||||
);
|
||||
assertThat(events.get(0).lifecycle()).isEqualTo(EventLifecycle.START);
|
||||
assertThat(events.get(2).lifecycle()).isEqualTo(EventLifecycle.END);
|
||||
assertThat(events.get(1).eventDetails().type()).isEqualTo("POSITION");
|
||||
}
|
||||
|
||||
private DriverExtractionSession driver() {
|
||||
return 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(new ExtractedSupportEvent(
|
||||
"SUP-1",
|
||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||
"POSITION",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
BigDecimal.valueOf(48.2082),
|
||||
BigDecimal.valueOf(16.3738),
|
||||
"AUTHENTIC",
|
||||
150L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"raw-path"
|
||||
)),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private TachographFileSession session(DriverExtractionSession driver) {
|
||||
return 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, 1, 1, 1, 1, 0),
|
||||
List.of(),
|
||||
Instant.now(),
|
||||
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.processing.model.UnifiedDriverTimelineRequest;
|
||||
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.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverKeyFactory;
|
||||
import at.procon.eventhub.tachographfilesession.service.EventBackedDriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.InMemoryTachographFileSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.IntervalBackedDriverTimelineEventBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.VehicleKeyFactory;
|
||||
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 UnifiedDriverTimelineServiceTest {
|
||||
|
||||
@Test
|
||||
void reconstructsTimelineThroughUnifiedService() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
EventBackedDriverTimelineBuilder eventBackedBuilder = new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
new at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder(),
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
);
|
||||
UnifiedDriverTimelineService service = new UnifiedDriverTimelineService(List.of(
|
||||
new TachographFileSessionUnifiedDriverTimelineSource(repository, eventBackedBuilder)
|
||||
));
|
||||
|
||||
DriverExtractionSession driver = driver();
|
||||
TachographFileSession session = session(driver);
|
||||
repository.save(session);
|
||||
|
||||
ResolvedDriverTimeline timeline = service.loadDriverTimeline(
|
||||
UnifiedDriverTimelineRequest.forTachographFileSession(session.sessionId(), driver.driverKey())
|
||||
);
|
||||
|
||||
assertThat(timeline.sourceKind()).isEqualTo("DRIVER_CARD");
|
||||
assertThat(timeline.loadedFrom()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:00:00Z"));
|
||||
assertThat(timeline.loadedTo()).isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
|
||||
assertThat(timeline.activityIntervals()).hasSize(1);
|
||||
assertThat(timeline.vehicleUsageIntervals()).hasSize(1);
|
||||
assertThat(timeline.supportEvents()).hasSize(1);
|
||||
assertThat(timeline.activityIntervals().get(0).activityType()).isEqualTo("DRIVE");
|
||||
assertThat(timeline.vehicleUsageIntervals().get(0).registrationKey()).isEqualTo("12:REG-1");
|
||||
}
|
||||
|
||||
private DriverExtractionSession driver() {
|
||||
return 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(new ExtractedSupportEvent(
|
||||
"SUP-1",
|
||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||
"POSITION",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
BigDecimal.valueOf(48.2082),
|
||||
BigDecimal.valueOf(16.3738),
|
||||
"AUTHENTIC",
|
||||
150L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"raw-path"
|
||||
)),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private TachographFileSession session(DriverExtractionSession driver) {
|
||||
return 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, 1, 1, 1, 1, 0),
|
||||
List.of(),
|
||||
Instant.now(),
|
||||
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,13 @@ class DriverCardXmlExtractionServiceTest {
|
|||
assertThat(driver.cardActivityIntervals().get(0).registrationKey()).isEqualTo("12:W-12345A");
|
||||
assertThat(driver.cardActivityIntervals().get(1).to()).isEqualTo(driver.cardVehicleUsageIntervals().get(0).to().plusSeconds(1));
|
||||
assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B");
|
||||
assertThat(driver.supportEvents()).hasSize(5);
|
||||
assertThat(driver.supportEvents()).extracting("eventDomain")
|
||||
.containsExactly("PLACE", "POSITION", "SPECIFIC_CONDITION", "BORDER_CROSSING", "LOAD_UNLOAD");
|
||||
assertThat(driver.supportEvents().get(2).eventType()).isEqualTo("FERRY_TRAIN");
|
||||
assertThat(driver.supportEvents().get(2).eventLifecycle()).isEqualTo("BEGIN");
|
||||
assertThat(driver.supportEvents().get(3).countryFrom()).isEqualTo("12");
|
||||
assertThat(driver.supportEvents().get(4).operation()).isEqualTo("UNLOAD");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -109,6 +109,92 @@ final class DriverCardXmlSamples {
|
|||
</cardDriverActivity>
|
||||
<signature></signature>
|
||||
</DriverActivityData>
|
||||
<Places>
|
||||
<cardPlaceDailyWorkPeriod>
|
||||
<placeRecords>
|
||||
<entryTime>2026-04-01T08:00:00Z</entryTime>
|
||||
<entryTypeDailyWorkPeriod>0</entryTypeDailyWorkPeriod>
|
||||
<dailyWorkPeriodCountry>12</dailyWorkPeriodCountry>
|
||||
<dailyWorkPeriodRegion>9</dailyWorkPeriodRegion>
|
||||
<vehicleOdometerValue>1000</vehicleOdometerValue>
|
||||
<entryGnssPlaceRecord>
|
||||
<timeStamp>2026-04-01T08:00:00Z</timeStamp>
|
||||
<gnssAccuracy>5</gnssAccuracy>
|
||||
<geoCoordinates>
|
||||
<latitude>48.2082</latitude>
|
||||
<longitude>16.3738</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>1</authenticationStatus>
|
||||
</entryGnssPlaceRecord>
|
||||
</placeRecords>
|
||||
</cardPlaceDailyWorkPeriod>
|
||||
<signature></signature>
|
||||
</Places>
|
||||
<SpecificConditions>
|
||||
<specificConditionData>
|
||||
<specificConditionRecord>
|
||||
<entryTime>2026-04-01T10:30:00Z</entryTime>
|
||||
<specificConditionType>3</specificConditionType>
|
||||
</specificConditionRecord>
|
||||
</specificConditionData>
|
||||
<signature></signature>
|
||||
</SpecificConditions>
|
||||
<GnssPlaces>
|
||||
<gnssAccumulatedDriving>
|
||||
<gnssAccumulatedDrivingRecord>
|
||||
<timeStamp>2026-04-01T09:30:00Z</timeStamp>
|
||||
<gnssPlaceRecord>
|
||||
<timeStamp>2026-04-01T09:30:00Z</timeStamp>
|
||||
<gnssAccuracy>4</gnssAccuracy>
|
||||
<geoCoordinates>
|
||||
<latitude>48.2000</latitude>
|
||||
<longitude>16.3600</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>1</authenticationStatus>
|
||||
</gnssPlaceRecord>
|
||||
<vehicleOdometerValue>1050</vehicleOdometerValue>
|
||||
</gnssAccumulatedDrivingRecord>
|
||||
</gnssAccumulatedDriving>
|
||||
<signature></signature>
|
||||
</GnssPlaces>
|
||||
<BorderCrossings>
|
||||
<cardBorderCrossings>
|
||||
<cardBorderCrossingRecord>
|
||||
<countryLeft>12</countryLeft>
|
||||
<countryEntered>56</countryEntered>
|
||||
<gnssPlaceAuthRecord>
|
||||
<timeStamp>2026-04-01T12:15:00Z</timeStamp>
|
||||
<gnssAccuracy>4</gnssAccuracy>
|
||||
<geoCoordinates>
|
||||
<latitude>48.3000</latitude>
|
||||
<longitude>16.5000</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>0</authenticationStatus>
|
||||
</gnssPlaceAuthRecord>
|
||||
<vehicleOdometerValue>1120</vehicleOdometerValue>
|
||||
</cardBorderCrossingRecord>
|
||||
</cardBorderCrossings>
|
||||
<signature></signature>
|
||||
</BorderCrossings>
|
||||
<LoadUnloadOperations>
|
||||
<cardLoadUnloadOperations>
|
||||
<cardLoadUnloadRecord>
|
||||
<timeStamp>2026-04-01T12:45:00Z</timeStamp>
|
||||
<operationType>2</operationType>
|
||||
<gnssPlaceAuthRecord>
|
||||
<timeStamp>2026-04-01T12:45:00Z</timeStamp>
|
||||
<gnssAccuracy>3</gnssAccuracy>
|
||||
<geoCoordinates>
|
||||
<latitude>48.3100</latitude>
|
||||
<longitude>16.5100</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>1</authenticationStatus>
|
||||
</gnssPlaceAuthRecord>
|
||||
<vehicleOdometerValue>1130</vehicleOdometerValue>
|
||||
</cardLoadUnloadRecord>
|
||||
</cardLoadUnloadOperations>
|
||||
<signature></signature>
|
||||
</LoadUnloadOperations>
|
||||
</DriverCard>
|
||||
""";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class DriverTimelineBuilderTest {
|
|||
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||
"PLACE",
|
||||
"BEGIN_DAILY_WORK_PERIOD",
|
||||
null,
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
|
|
@ -87,6 +88,11 @@ class DriverTimelineBuilderTest {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"d"
|
||||
)),
|
||||
List.of(new ExtractionWarning("W1", "warning", "/x"))
|
||||
|
|
|
|||
|
|
@ -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,116 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
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.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
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 EventBackedDriverTimelineBuilderTest {
|
||||
|
||||
private final DriverTimelineBuilder directBuilder = new DriverTimelineBuilder();
|
||||
private final EventBackedDriverTimelineBuilder eventBackedBuilder =
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
directBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
);
|
||||
|
||||
@Test
|
||||
void reconstructsTimelineFromEvents() {
|
||||
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(new ExtractedSupportEvent(
|
||||
"SUP-1",
|
||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||
"POSITION",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
BigDecimal.valueOf(48.2082),
|
||||
BigDecimal.valueOf(16.3738),
|
||||
"AUTHENTIC",
|
||||
150L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"raw-path"
|
||||
)),
|
||||
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, 1, 1, 1, 1, 0),
|
||||
List.of(),
|
||||
Instant.now(),
|
||||
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||
);
|
||||
|
||||
ResolvedDriverTimeline direct = directBuilder.build(session, driver);
|
||||
ResolvedDriverTimeline reconstructed = eventBackedBuilder.build(session, driver);
|
||||
|
||||
assertThat(reconstructed.sourceKind()).isEqualTo(direct.sourceKind());
|
||||
assertThat(reconstructed.loadedFrom()).isEqualTo(direct.loadedFrom());
|
||||
assertThat(reconstructed.loadedTo()).isEqualTo(direct.loadedTo());
|
||||
assertThat(reconstructed.activityIntervals()).containsExactlyElementsOf(direct.activityIntervals());
|
||||
assertThat(reconstructed.vehicleUsageIntervals()).containsExactlyElementsOf(direct.vehicleUsageIntervals());
|
||||
assertThat(reconstructed.supportEvents()).containsExactlyElementsOf(direct.supportEvents());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
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",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
BigDecimal.valueOf(48.2082),
|
||||
BigDecimal.valueOf(16.3738),
|
||||
"AUTHENTIC",
|
||||
150L,
|
||||
null,
|
||||
null,
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package at.procon.eventhub.tachographfilesession.service;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.service.EventDetailsFactory;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographOperatingPeriodsProcessingRequest;
|
||||
|
|
@ -19,6 +20,7 @@ import java.time.temporal.ChronoUnit;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TachographFileSessionProcessingServiceTest {
|
||||
|
|
@ -27,9 +29,19 @@ 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),
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
),
|
||||
properties
|
||||
);
|
||||
|
||||
|
|
@ -99,9 +111,19 @@ 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),
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
),
|
||||
properties
|
||||
);
|
||||
|
||||
|
|
@ -187,13 +209,109 @@ class TachographFileSessionProcessingServiceTest {
|
|||
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canUseEventBackedTimelineModeForEsperProcessing() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
properties.getTachographFileSession().getProcessing().setTimelineInputMode(EventHubProperties.TimelineInputMode.EVENTS);
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder();
|
||||
TachographFileSessionProcessingService service = new TachographFileSessionProcessingService(
|
||||
repository,
|
||||
driverTimelineBuilder,
|
||||
new DriverTimelineReusableProjectionBuilder(driverTimelineBuilder),
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
),
|
||||
properties
|
||||
);
|
||||
|
||||
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-01T11:00:00Z"),
|
||||
100L,
|
||||
200L,
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"vu-1"
|
||||
),
|
||||
new ExtractedCardVehicleUsageInterval(
|
||||
"CVU-2",
|
||||
OffsetDateTime.parse("2026-05-01T12:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T13:00:00Z"),
|
||||
201L,
|
||||
260L,
|
||||
"12:REG-2",
|
||||
"VIN-2",
|
||||
"vu-2"
|
||||
)
|
||||
),
|
||||
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"),
|
||||
new ExtractedCardActivityInterval("ACT-2", OffsetDateTime.parse("2026-05-01T09:00:00Z"), OffsetDateTime.parse("2026-05-01T10:00:00Z"), "WORK", "DRIVER", "INSERTED", "SINGLE", "12:REG-1", "VIN-1", "b"),
|
||||
new ExtractedCardActivityInterval("ACT-3", OffsetDateTime.parse("2026-05-01T10:00:00Z"), OffsetDateTime.parse("2026-05-01T10:05:00Z"), "DRIVE", "DRIVER", "INSERTED", "SINGLE", "12:REG-2", "VIN-2", "c")
|
||||
),
|
||||
List.of(),
|
||||
List.of()
|
||||
);
|
||||
TachographFileSession session = new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "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)
|
||||
);
|
||||
repository.save(session);
|
||||
|
||||
TachographEsperDriverProcessingResultDto result = service.getEsperDriverProcessingResults(
|
||||
session.sessionId(),
|
||||
driver.driverKey(),
|
||||
new TachographEsperEventsProcessingRequest(
|
||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T12:30:00Z"),
|
||||
3,
|
||||
720
|
||||
)
|
||||
);
|
||||
|
||||
assertThat(result.activityIntervalCount()).isEqualTo(3);
|
||||
assertThat(result.drivingIntervalCount()).isEqualTo(2);
|
||||
assertThat(result.drivingInterruptionIntervalCount()).isEqualTo(1);
|
||||
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(2);
|
||||
assertThat(result.vuCardAbsentIntervalCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
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),
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
),
|
||||
properties
|
||||
);
|
||||
|
||||
|
|
@ -272,9 +390,19 @@ 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),
|
||||
new EventBackedDriverTimelineBuilder(
|
||||
new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
)
|
||||
),
|
||||
properties
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,17 @@ class VehicleUnitXmlExtractionServiceTest {
|
|||
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to()).isNull();
|
||||
assertThat(secondDriver.cardActivityIntervals()).hasSize(1);
|
||||
assertThat(secondDriver.cardActivityIntervals().get(0).from().toString()).isEqualTo("2026-04-02T07:30Z");
|
||||
assertThat(secondDriver.supportEvents()).hasSize(2);
|
||||
assertThat(secondDriver.supportEvents()).extracting("eventDomain").containsExactly("POSITION", "SPECIFIC_CONDITION");
|
||||
assertThat(secondDriver.supportEvents()).hasSize(6);
|
||||
assertThat(secondDriver.supportEvents()).extracting("eventDomain")
|
||||
.containsExactly("POSITION", "SPECIFIC_CONDITION", "BORDER_CROSSING", "LOAD_UNLOAD", "SPEEDING", "SPEEDING");
|
||||
assertThat(secondDriver.supportEvents().get(0).latitude()).isNotNull();
|
||||
assertThat(secondDriver.supportEvents().get(1).code()).isEqualTo("1");
|
||||
assertThat(secondDriver.supportEvents().get(1).eventType()).isEqualTo("OUT");
|
||||
assertThat(secondDriver.supportEvents().get(1).eventLifecycle()).isEqualTo("BEGIN");
|
||||
assertThat(secondDriver.supportEvents().get(2).countryFrom()).isEqualTo("12");
|
||||
assertThat(secondDriver.supportEvents().get(2).countryTo()).isEqualTo("56");
|
||||
assertThat(secondDriver.supportEvents().get(3).operation()).isEqualTo("LOAD");
|
||||
assertThat(secondDriver.supportEvents().get(4).eventLifecycle()).isEqualTo("BEGIN");
|
||||
assertThat(secondDriver.supportEvents().get(5).eventLifecycle()).isEqualTo("END");
|
||||
|
||||
assertThat(session.warnings()).isEmpty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,80 @@ final class VehicleUnitXmlSamples {
|
|||
<specificConditionType>1</specificConditionType>
|
||||
</specificConditionRecords>
|
||||
</vuSpecificConditionData>
|
||||
<vuBorderCrossingData>
|
||||
<vuBorderCrossingRecord>
|
||||
<cardNumberDriverSlot>
|
||||
<cardType>DRIVER_CARD</cardType>
|
||||
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||
<cardNumber>
|
||||
<driverIdentification>999999999999</driverIdentification>
|
||||
<cardReplacementIndex>1</cardReplacementIndex>
|
||||
<cardRenewalIndex>1</cardRenewalIndex>
|
||||
</cardNumber>
|
||||
<generation>2</generation>
|
||||
</cardNumberDriverSlot>
|
||||
<countryLeft>12</countryLeft>
|
||||
<countryEntered>56</countryEntered>
|
||||
<gnssPlaceAuthRecord>
|
||||
<timeStamp>2026-04-02T08:30:00Z</timeStamp>
|
||||
<geoCoordinates>
|
||||
<latitude>48020</latitude>
|
||||
<longitude>16380</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>AUTHENTICATED</authenticationStatus>
|
||||
</gnssPlaceAuthRecord>
|
||||
<vehicleOdometerValue>1220</vehicleOdometerValue>
|
||||
</vuBorderCrossingRecord>
|
||||
</vuBorderCrossingData>
|
||||
<vuLoadUnloadData>
|
||||
<vuLoadUnloadRecord>
|
||||
<timeStamp>2026-04-02T08:45:00Z</timeStamp>
|
||||
<operationType>1</operationType>
|
||||
<cardNumberDriverSlot>
|
||||
<cardType>DRIVER_CARD</cardType>
|
||||
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||
<cardNumber>
|
||||
<driverIdentification>999999999999</driverIdentification>
|
||||
<cardReplacementIndex>1</cardReplacementIndex>
|
||||
<cardRenewalIndex>1</cardRenewalIndex>
|
||||
</cardNumber>
|
||||
<generation>2</generation>
|
||||
</cardNumberDriverSlot>
|
||||
<gnssPlaceAuthRecord>
|
||||
<timeStamp>2026-04-02T08:45:00Z</timeStamp>
|
||||
<geoCoordinates>
|
||||
<latitude>48025</latitude>
|
||||
<longitude>16385</longitude>
|
||||
</geoCoordinates>
|
||||
<authenticationStatus>AUTHENTICATED</authenticationStatus>
|
||||
</gnssPlaceAuthRecord>
|
||||
<vehicleOdometerValue>1225</vehicleOdometerValue>
|
||||
</vuLoadUnloadRecord>
|
||||
</vuLoadUnloadData>
|
||||
</Activities>
|
||||
<EventsFaults>
|
||||
<vuOverSpeedingEventData>
|
||||
<vuOverSpeedingEventRecords>
|
||||
<eventType>1</eventType>
|
||||
<eventRecordPurpose>1</eventRecordPurpose>
|
||||
<eventBeginTime>2026-04-02T08:10:00Z</eventBeginTime>
|
||||
<eventEndTime>2026-04-02T08:12:00Z</eventEndTime>
|
||||
<maxSpeedValue>96</maxSpeedValue>
|
||||
<averageSpeedValue>91</averageSpeedValue>
|
||||
<cardNumberDriverSlotBegin>
|
||||
<cardType>DRIVER_CARD</cardType>
|
||||
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||
<cardNumber>
|
||||
<driverIdentification>999999999999</driverIdentification>
|
||||
<cardReplacementIndex>1</cardReplacementIndex>
|
||||
<cardRenewalIndex>1</cardRenewalIndex>
|
||||
</cardNumber>
|
||||
<generation>2</generation>
|
||||
</cardNumberDriverSlotBegin>
|
||||
<similarEventsNumber>1</similarEventsNumber>
|
||||
</vuOverSpeedingEventRecords>
|
||||
</vuOverSpeedingEventData>
|
||||
</EventsFaults>
|
||||
</VehicleUnit>
|
||||
""";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue