Add and optimize tachograph session support events
This commit is contained in:
parent
2ded38a28a
commit
dd0ccae290
|
|
@ -8,16 +8,22 @@ public record ExtractedSupportEvent(
|
||||||
OffsetDateTime occurredAt,
|
OffsetDateTime occurredAt,
|
||||||
String eventDomain,
|
String eventDomain,
|
||||||
String eventType,
|
String eventType,
|
||||||
|
String eventLifecycle,
|
||||||
String slot,
|
String slot,
|
||||||
String registrationKey,
|
String registrationKey,
|
||||||
String vehicleKey,
|
String vehicleKey,
|
||||||
String country,
|
String country,
|
||||||
String region,
|
String region,
|
||||||
|
String countryFrom,
|
||||||
|
String countryTo,
|
||||||
|
String operation,
|
||||||
BigDecimal latitude,
|
BigDecimal latitude,
|
||||||
BigDecimal longitude,
|
BigDecimal longitude,
|
||||||
String authenticationStatus,
|
String authenticationStatus,
|
||||||
Long odometerKm,
|
Long odometerKm,
|
||||||
String code,
|
String code,
|
||||||
|
BigDecimal avgSpeedKmh,
|
||||||
|
BigDecimal maxSpeedKmh,
|
||||||
String rawRecordPath
|
String rawRecordPath
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInter
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
|
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.ExtractedVehicle;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
|
|
@ -71,6 +73,7 @@ public class DriverCardXmlExtractionService {
|
||||||
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
|
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
|
||||||
List<ExtractedCardActivityInterval> activityIntervals =
|
List<ExtractedCardActivityInterval> activityIntervals =
|
||||||
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
|
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
|
||||||
|
List<ExtractedSupportEvent> supportEvents = extractSupportEvents(document, vehicleUsageIntervals, warnings);
|
||||||
|
|
||||||
DriverExtractionSession driverSession = new DriverExtractionSession(
|
DriverExtractionSession driverSession = new DriverExtractionSession(
|
||||||
driverKey,
|
driverKey,
|
||||||
|
|
@ -80,7 +83,7 @@ public class DriverCardXmlExtractionService {
|
||||||
List.copyOf(vehiclesByKey.values()),
|
List.copyOf(vehiclesByKey.values()),
|
||||||
List.copyOf(vehicleUsageIntervals),
|
List.copyOf(vehicleUsageIntervals),
|
||||||
List.copyOf(activityIntervals),
|
List.copyOf(activityIntervals),
|
||||||
List.of(),
|
List.copyOf(supportEvents),
|
||||||
List.copyOf(warnings)
|
List.copyOf(warnings)
|
||||||
);
|
);
|
||||||
Map<String, DriverExtractionSession> driversByKey = Map.of(driverKey, driverSession);
|
Map<String, DriverExtractionSession> driversByKey = Map.of(driverKey, driverSession);
|
||||||
|
|
@ -302,6 +305,284 @@ public class DriverCardXmlExtractionService {
|
||||||
return result;
|
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(
|
private List<ExtractedCardActivityInterval> splitByVehicleCoverage(
|
||||||
ExtractedCardActivityInterval interval,
|
ExtractedCardActivityInterval interval,
|
||||||
List<ExtractedCardVehicleUsageInterval> overlappingUsages
|
List<ExtractedCardVehicleUsageInterval> overlappingUsages
|
||||||
|
|
@ -366,6 +647,26 @@ public class DriverCardXmlExtractionService {
|
||||||
return usage.to().plusSeconds(1);
|
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) {
|
private Element child(Element parent, String name) {
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -456,6 +757,50 @@ public class DriverCardXmlExtractionService {
|
||||||
return value.trim().toUpperCase().replace('-', '_').replace(' ', '_');
|
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(
|
private record ActivityChange(
|
||||||
OffsetDateTime from,
|
OffsetDateTime from,
|
||||||
String activityType,
|
String activityType,
|
||||||
|
|
@ -484,4 +829,42 @@ public class DriverCardXmlExtractionService {
|
||||||
}
|
}
|
||||||
return builder.toString();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,16 +152,22 @@ public class EventBackedDriverTimelineBuilder {
|
||||||
event.occurredAt(),
|
event.occurredAt(),
|
||||||
event.eventDomain().name(),
|
event.eventDomain().name(),
|
||||||
text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),
|
text(raw, "supportEventType") == null ? event.eventType().name() : text(raw, "supportEventType"),
|
||||||
|
event.lifecycle().name(),
|
||||||
text(raw, "slot"),
|
text(raw, "slot"),
|
||||||
text(raw, "registrationKey"),
|
text(raw, "registrationKey"),
|
||||||
text(raw, "vehicleKey"),
|
text(raw, "vehicleKey"),
|
||||||
text(raw, "country"),
|
text(raw, "country"),
|
||||||
text(raw, "region"),
|
text(raw, "region"),
|
||||||
|
text(raw, "countryFrom"),
|
||||||
|
text(raw, "countryTo"),
|
||||||
|
text(raw, "operation"),
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
text(raw, "authenticationStatus"),
|
text(raw, "authenticationStatus"),
|
||||||
longValue(raw, "odometerKm"),
|
longValue(raw, "odometerKm"),
|
||||||
text(raw, "code"),
|
text(raw, "code"),
|
||||||
|
decimal(raw, "avgSpeedKmh"),
|
||||||
|
decimal(raw, "maxSpeedKmh"),
|
||||||
text(raw, "rawRecordPath")
|
text(raw, "rawRecordPath")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +295,21 @@ public class EventBackedDriverTimelineBuilder {
|
||||||
return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
|
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) {
|
private List<String> stringList(JsonNode node, String field) {
|
||||||
if (node == null || field == null) {
|
if (node == null || field == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
||||||
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||||
EventDomain eventDomain = supportEventDomain(supportEvent.eventDomain());
|
EventDomain eventDomain = supportEventDomain(supportEvent.eventDomain());
|
||||||
EventType eventType = supportEventType(eventDomain, supportEvent.eventType(), supportEvent.code());
|
EventType eventType = supportEventType(eventDomain, supportEvent.eventType(), supportEvent.code());
|
||||||
EventLifecycle lifecycle = supportEventLifecycle(eventDomain, supportEvent.eventType());
|
EventLifecycle lifecycle = supportEventLifecycle(eventDomain, supportEvent.eventType(), supportEvent.eventLifecycle());
|
||||||
boolean manualEntry = isManualPlaceEvent(supportEvent.eventType());
|
boolean manualEntry = isManualPlaceEvent(supportEvent.eventType());
|
||||||
VehicleRefDto vehicleRef = vehicleRef(
|
VehicleRefDto vehicleRef = vehicleRef(
|
||||||
supportEvent.registrationKey(),
|
supportEvent.registrationKey(),
|
||||||
|
|
@ -312,9 +312,14 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
||||||
raw.put("vehicleKey", supportEvent.vehicleKey());
|
raw.put("vehicleKey", supportEvent.vehicleKey());
|
||||||
raw.put("country", supportEvent.country());
|
raw.put("country", supportEvent.country());
|
||||||
raw.put("region", supportEvent.region());
|
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("authenticationStatus", supportEvent.authenticationStatus());
|
||||||
raw.put("odometerKm", supportEvent.odometerKm());
|
raw.put("odometerKm", supportEvent.odometerKm());
|
||||||
raw.put("code", supportEvent.code());
|
raw.put("code", supportEvent.code());
|
||||||
|
raw.put("avgSpeedKmh", supportEvent.avgSpeedKmh());
|
||||||
|
raw.put("maxSpeedKmh", supportEvent.maxSpeedKmh());
|
||||||
raw.put("rawRecordPath", supportEvent.rawRecordPath());
|
raw.put("rawRecordPath", supportEvent.rawRecordPath());
|
||||||
|
|
||||||
events.add(event(
|
events.add(event(
|
||||||
|
|
@ -509,12 +514,19 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
||||||
return switch (normalizeToken(value)) {
|
return switch (normalizeToken(value)) {
|
||||||
case "PLACE" -> EventDomain.PLACE;
|
case "PLACE" -> EventDomain.PLACE;
|
||||||
case "POSITION" -> EventDomain.POSITION;
|
case "POSITION" -> EventDomain.POSITION;
|
||||||
|
case "BORDER_CROSSING" -> EventDomain.BORDER_CROSSING;
|
||||||
|
case "LOAD_UNLOAD" -> EventDomain.LOAD_UNLOAD;
|
||||||
case "SPECIFIC_CONDITION" -> EventDomain.SPECIFIC_CONDITION;
|
case "SPECIFIC_CONDITION" -> EventDomain.SPECIFIC_CONDITION;
|
||||||
|
case "SPEEDING" -> EventDomain.SPEEDING;
|
||||||
default -> EventDomain.TELEMATICS_DATA;
|
default -> EventDomain.TELEMATICS_DATA;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private EventType supportEventType(EventDomain eventDomain, String eventType, String code) {
|
private EventType supportEventType(EventDomain eventDomain, String eventType, String code) {
|
||||||
|
EventType explicit = parseEnum(EventType.class, eventType, null);
|
||||||
|
if (explicit != null) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
return switch (eventDomain) {
|
return switch (eventDomain) {
|
||||||
case PLACE -> EventType.WORKING_DAY_PLACE_RECORDED;
|
case PLACE -> EventType.WORKING_DAY_PLACE_RECORDED;
|
||||||
case POSITION -> EventType.POSITION_RECORDED;
|
case POSITION -> EventType.POSITION_RECORDED;
|
||||||
|
|
@ -528,11 +540,18 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
||||||
}
|
}
|
||||||
yield EventType.UNKNOWN_EVENT;
|
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);
|
default -> parseEnum(EventType.class, eventType, EventType.UNKNOWN_EVENT);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private EventLifecycle supportEventLifecycle(EventDomain eventDomain, String eventType) {
|
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) {
|
if (eventDomain == EventDomain.PLACE) {
|
||||||
String normalized = normalizeToken(eventType);
|
String normalized = normalizeToken(eventType);
|
||||||
if (normalized != null && normalized.startsWith("BEGIN")) {
|
if (normalized != null && normalized.startsWith("BEGIN")) {
|
||||||
|
|
@ -553,8 +572,11 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
||||||
private EventDetailsDto supportDetails(EventDomain eventDomain, ExtractedSupportEvent supportEvent) {
|
private EventDetailsDto supportDetails(EventDomain eventDomain, ExtractedSupportEvent supportEvent) {
|
||||||
return switch (eventDomain) {
|
return switch (eventDomain) {
|
||||||
case PLACE -> detailsFactory.place(supportEvent.country(), supportEvent.region());
|
case PLACE -> detailsFactory.place(supportEvent.country(), supportEvent.region());
|
||||||
case POSITION -> detailsFactory.position(supportEvent.eventType());
|
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 SPECIFIC_CONDITION -> detailsFactory.specificCondition();
|
||||||
|
case SPEEDING -> detailsFactory.speeding(supportEvent.avgSpeedKmh(), supportEvent.maxSpeedKmh(), null);
|
||||||
default -> new EventDetailsDto("TACHOGRAPH_SUPPORT", detailsFactory.payloadFromMap(Map.of()));
|
default -> new EventDetailsDto("TACHOGRAPH_SUPPORT", detailsFactory.payloadFromMap(Map.of()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,9 @@ public class VehicleUnitXmlExtractionService {
|
||||||
extractVuPlaceSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
extractVuPlaceSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||||
extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
extractVuGnssSupportEvents(document, vehicleContext, vuCardIwIntervals, driversByKey, warnings);
|
||||||
extractVuSpecificConditionSupportEvents(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(
|
private void extractVuPlaceSupportEvents(
|
||||||
|
|
@ -415,16 +418,22 @@ public class VehicleUnitXmlExtractionService {
|
||||||
occurredAt,
|
occurredAt,
|
||||||
"PLACE",
|
"PLACE",
|
||||||
mapPlaceEntryType(entryType),
|
mapPlaceEntryType(entryType),
|
||||||
|
null,
|
||||||
assignment.slot(),
|
assignment.slot(),
|
||||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||||
text(record, "placeRecord/dailyWorkPeriodCountry"),
|
text(record, "placeRecord/dailyWorkPeriodCountry"),
|
||||||
text(record, "placeRecord/dailyWorkPeriodRegion"),
|
text(record, "placeRecord/dailyWorkPeriodRegion"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
authenticationStatus,
|
authenticationStatus,
|
||||||
longValue(text(record, "placeRecord/vehicleOdometerValue")),
|
longValue(text(record, "placeRecord/vehicleOdometerValue")),
|
||||||
normalizeToken(entryType),
|
normalizeToken(entryType),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
path
|
path
|
||||||
),
|
),
|
||||||
warnings
|
warnings
|
||||||
|
|
@ -480,17 +489,23 @@ public class VehicleUnitXmlExtractionService {
|
||||||
"VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
"VUGNSS-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||||
occurredAt,
|
occurredAt,
|
||||||
"POSITION",
|
"POSITION",
|
||||||
"GNSS_ACCUMULATED_DRIVING",
|
"POSITION_RECORDED",
|
||||||
|
"SNAPSHOT",
|
||||||
assignment.slot(),
|
assignment.slot(),
|
||||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
authenticationStatus,
|
authenticationStatus,
|
||||||
longValue(text(record, "vehicleOdometerValue")),
|
longValue(text(record, "vehicleOdometerValue")),
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
path
|
path
|
||||||
),
|
),
|
||||||
warnings
|
warnings
|
||||||
|
|
@ -531,6 +546,7 @@ public class VehicleUnitXmlExtractionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
String conditionCode = normalizeToken(text(record, "specificConditionType"));
|
String conditionCode = normalizeToken(text(record, "specificConditionType"));
|
||||||
|
String[] specificCondition = mapSpecificCondition(conditionCode);
|
||||||
for (DriverAssignment assignment : assignments) {
|
for (DriverAssignment assignment : assignments) {
|
||||||
addSupportEvent(
|
addSupportEvent(
|
||||||
driversByKey,
|
driversByKey,
|
||||||
|
|
@ -539,7 +555,8 @@ public class VehicleUnitXmlExtractionService {
|
||||||
"VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
"VUSC-" + (i + 1) + "-" + assignment.driverKey() + "-" + assignment.slot(),
|
||||||
occurredAt,
|
occurredAt,
|
||||||
"SPECIFIC_CONDITION",
|
"SPECIFIC_CONDITION",
|
||||||
"SPECIFIC_CONDITION",
|
specificCondition[0],
|
||||||
|
specificCondition[1],
|
||||||
assignment.slot(),
|
assignment.slot(),
|
||||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||||
|
|
@ -549,7 +566,12 @@ public class VehicleUnitXmlExtractionService {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
conditionCode,
|
conditionCode,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
path
|
path
|
||||||
),
|
),
|
||||||
warnings
|
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) {
|
private boolean isPartiallyCovered(ExtractedCardActivityInterval interval, List<ActivitySegment> segments) {
|
||||||
if (segments.isEmpty()) {
|
if (segments.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -696,6 +959,13 @@ public class VehicleUnitXmlExtractionService {
|
||||||
return Long.parseLong(value.trim());
|
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) {
|
private BigDecimal geoCoordinate(Object node, String expression, boolean latitude) {
|
||||||
String value = text(node, expression);
|
String value = text(node, expression);
|
||||||
if (value == null) {
|
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) {
|
private String joinCardNumber(Element node, String basePath) {
|
||||||
String driverIdentification = text(node, basePath + "/driverIdentification");
|
String driverIdentification = text(node, basePath + "/driverIdentification");
|
||||||
if (driverIdentification == null) {
|
if (driverIdentification == null) {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,13 @@ class DriverCardXmlExtractionServiceTest {
|
||||||
assertThat(driver.cardActivityIntervals().get(0).registrationKey()).isEqualTo("12:W-12345A");
|
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(1).to()).isEqualTo(driver.cardVehicleUsageIntervals().get(0).to().plusSeconds(1));
|
||||||
assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B");
|
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
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,92 @@ final class DriverCardXmlSamples {
|
||||||
</cardDriverActivity>
|
</cardDriverActivity>
|
||||||
<signature></signature>
|
<signature></signature>
|
||||||
</DriverActivityData>
|
</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>
|
</DriverCard>
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ class DriverTimelineBuilderTest {
|
||||||
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||||
"PLACE",
|
"PLACE",
|
||||||
"BEGIN_DAILY_WORK_PERIOD",
|
"BEGIN_DAILY_WORK_PERIOD",
|
||||||
|
null,
|
||||||
"DRIVER",
|
"DRIVER",
|
||||||
"12:REG-1",
|
"12:REG-1",
|
||||||
"VIN-1",
|
"VIN-1",
|
||||||
|
|
@ -87,6 +88,11 @@ class DriverTimelineBuilderTest {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
"d"
|
"d"
|
||||||
)),
|
)),
|
||||||
List.of(new ExtractionWarning("W1", "warning", "/x"))
|
List.of(new ExtractionWarning("W1", "warning", "/x"))
|
||||||
|
|
|
||||||
|
|
@ -72,17 +72,23 @@ class EventBackedDriverTimelineBuilderTest {
|
||||||
"SUP-1",
|
"SUP-1",
|
||||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||||
"POSITION",
|
"POSITION",
|
||||||
"GNSS_ACCUMULATED_DRIVING",
|
"POSITION_RECORDED",
|
||||||
|
"SNAPSHOT",
|
||||||
"DRIVER",
|
"DRIVER",
|
||||||
"12:REG-1",
|
"12:REG-1",
|
||||||
"VIN-1",
|
"VIN-1",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
BigDecimal.valueOf(48.2082),
|
BigDecimal.valueOf(48.2082),
|
||||||
BigDecimal.valueOf(16.3738),
|
BigDecimal.valueOf(16.3738),
|
||||||
"AUTHENTIC",
|
"AUTHENTIC",
|
||||||
150L,
|
150L,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
"raw-path"
|
"raw-path"
|
||||||
)),
|
)),
|
||||||
List.of()
|
List.of()
|
||||||
|
|
|
||||||
|
|
@ -110,17 +110,23 @@ class IntervalBackedDriverTimelineEventBuilderTest {
|
||||||
"VUGNSS-1",
|
"VUGNSS-1",
|
||||||
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
OffsetDateTime.parse("2026-05-01T08:45:00Z"),
|
||||||
"POSITION",
|
"POSITION",
|
||||||
"GNSS_ACCUMULATED_DRIVING",
|
"POSITION_RECORDED",
|
||||||
|
"SNAPSHOT",
|
||||||
"DRIVER",
|
"DRIVER",
|
||||||
"12:REG-1",
|
"12:REG-1",
|
||||||
"VIN-1",
|
"VIN-1",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
BigDecimal.valueOf(48.2082),
|
BigDecimal.valueOf(48.2082),
|
||||||
BigDecimal.valueOf(16.3738),
|
BigDecimal.valueOf(16.3738),
|
||||||
"AUTHENTIC",
|
"AUTHENTIC",
|
||||||
150L,
|
150L,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
"raw-path"
|
"raw-path"
|
||||||
)),
|
)),
|
||||||
List.of()
|
List.of()
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,17 @@ class VehicleUnitXmlExtractionServiceTest {
|
||||||
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to()).isNull();
|
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to()).isNull();
|
||||||
assertThat(secondDriver.cardActivityIntervals()).hasSize(1);
|
assertThat(secondDriver.cardActivityIntervals()).hasSize(1);
|
||||||
assertThat(secondDriver.cardActivityIntervals().get(0).from().toString()).isEqualTo("2026-04-02T07:30Z");
|
assertThat(secondDriver.cardActivityIntervals().get(0).from().toString()).isEqualTo("2026-04-02T07:30Z");
|
||||||
assertThat(secondDriver.supportEvents()).hasSize(2);
|
assertThat(secondDriver.supportEvents()).hasSize(6);
|
||||||
assertThat(secondDriver.supportEvents()).extracting("eventDomain").containsExactly("POSITION", "SPECIFIC_CONDITION");
|
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(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();
|
assertThat(session.warnings()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,80 @@ final class VehicleUnitXmlSamples {
|
||||||
<specificConditionType>1</specificConditionType>
|
<specificConditionType>1</specificConditionType>
|
||||||
</specificConditionRecords>
|
</specificConditionRecords>
|
||||||
</vuSpecificConditionData>
|
</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>
|
</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>
|
</VehicleUnit>
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue