Add DTI enrichment evaluation endpoint
This commit is contained in:
parent
94e1227ab3
commit
7be6a08013
|
|
@ -64,7 +64,7 @@
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/api/eventhub/esper-poc/tachograph/operating-period-evaluation?tenantKey={{tenantKey}}&driverId={{driverId}}&occurredFrom={{occurredFrom}}&occurredTo={{occurredTo}}&guardHours=24&operatingSplitIdleHours=7&significantDrivingMinutes=3&mergeGapSeconds=0&gapDetectionToleranceSeconds=0&unknownTreatmentMode=AS_BREAK_REST",
|
"raw": "{{baseUrl}}/api/eventhub/esper-poc/tachograph/operating-period-evaluation?tenantKey={{tenantKey}}&driverId={{driverId}}&occurredFrom={{occurredFrom}}&occurredTo={{occurredTo}}&guardHours=24&operatingSplitIdleHours=7&significantDrivingMinutes=3&mergeGapSeconds=0&gapDetectionToleranceSeconds=0&sourceSelectionMode=MIXED&unknownTreatmentMode=AS_BREAK_REST",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{baseUrl}}"
|
||||||
],
|
],
|
||||||
|
|
@ -112,6 +112,10 @@
|
||||||
"key": "gapDetectionToleranceSeconds",
|
"key": "gapDetectionToleranceSeconds",
|
||||||
"value": "0"
|
"value": "0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "sourceSelectionMode",
|
||||||
|
"value": "MIXED"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "unknownTreatmentMode",
|
"key": "unknownTreatmentMode",
|
||||||
"value": "AS_BREAK_REST"
|
"value": "AS_BREAK_REST"
|
||||||
|
|
@ -119,6 +123,84 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Evaluate tachograph DTI enrichment",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/eventhub/esper-poc/tachograph/dti-enrichment?tenantKey={{tenantKey}}&driverId={{driverId}}&occurredFrom={{occurredFrom}}&occurredTo={{occurredTo}}&guardHours=24&operatingSplitIdleHours=7&significantDrivingMinutes=3&mergeGapSeconds=0&gapDetectionToleranceSeconds=0&sourceSelectionMode=MIXED&unknownTreatmentMode=AS_BREAK_REST&vehicleEvidenceLookbackHours=720&geoSearchWindowMinutes=180&vicinityWindowMinutes=180",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"eventhub",
|
||||||
|
"esper-poc",
|
||||||
|
"tachograph",
|
||||||
|
"dti-enrichment"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "tenantKey",
|
||||||
|
"value": "{{tenantKey}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "driverId",
|
||||||
|
"value": "{{driverId}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "occurredFrom",
|
||||||
|
"value": "{{occurredFrom}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "occurredTo",
|
||||||
|
"value": "{{occurredTo}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "guardHours",
|
||||||
|
"value": "24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "operatingSplitIdleHours",
|
||||||
|
"value": "7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "significantDrivingMinutes",
|
||||||
|
"value": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "mergeGapSeconds",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "gapDetectionToleranceSeconds",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sourceSelectionMode",
|
||||||
|
"value": "MIXED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "unknownTreatmentMode",
|
||||||
|
"value": "AS_BREAK_REST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vehicleEvidenceLookbackHours",
|
||||||
|
"value": "720"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "geoSearchWindowMinutes",
|
||||||
|
"value": "180"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vicinityWindowMinutes",
|
||||||
|
"value": "180"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"variable": [
|
"variable": [
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
package at.procon.eventhub.esperpoc.api;
|
package at.procon.eventhub.esperpoc.api;
|
||||||
|
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperActivityMergeMode;
|
import at.procon.eventhub.esperpoc.dto.EsperActivityMergeMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentRequest;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentResultDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperPocRequest;
|
import at.procon.eventhub.esperpoc.dto.EsperPocRequest;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperPocResultDto;
|
import at.procon.eventhub.esperpoc.dto.EsperPocResultDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperShiftResolutionMode;
|
import at.procon.eventhub.esperpoc.dto.EsperShiftResolutionMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
||||||
|
import at.procon.eventhub.esperpoc.service.EsperDtiEnrichmentService;
|
||||||
import at.procon.eventhub.esperpoc.service.EsperOperatingPeriodEvaluationService;
|
import at.procon.eventhub.esperpoc.service.EsperOperatingPeriodEvaluationService;
|
||||||
import at.procon.eventhub.esperpoc.service.EsperPocDriverCardActivityService;
|
import at.procon.eventhub.esperpoc.service.EsperPocDriverCardActivityService;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
|
@ -25,13 +29,16 @@ public class EsperPocController {
|
||||||
|
|
||||||
private final EsperPocDriverCardActivityService service;
|
private final EsperPocDriverCardActivityService service;
|
||||||
private final EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService;
|
private final EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService;
|
||||||
|
private final EsperDtiEnrichmentService dtiEnrichmentService;
|
||||||
|
|
||||||
public EsperPocController(
|
public EsperPocController(
|
||||||
EsperPocDriverCardActivityService service,
|
EsperPocDriverCardActivityService service,
|
||||||
EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService
|
EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService,
|
||||||
|
EsperDtiEnrichmentService dtiEnrichmentService
|
||||||
) {
|
) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
this.operatingPeriodEvaluationService = operatingPeriodEvaluationService;
|
this.operatingPeriodEvaluationService = operatingPeriodEvaluationService;
|
||||||
|
this.dtiEnrichmentService = dtiEnrichmentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tachograph/driver-card-activities")
|
@GetMapping("/tachograph/driver-card-activities")
|
||||||
|
|
@ -77,6 +84,7 @@ public class EsperPocController {
|
||||||
@RequestParam(required = false) Integer significantDrivingMinutes,
|
@RequestParam(required = false) Integer significantDrivingMinutes,
|
||||||
@RequestParam(required = false) Integer mergeGapSeconds,
|
@RequestParam(required = false) Integer mergeGapSeconds,
|
||||||
@RequestParam(required = false) Integer gapDetectionToleranceSeconds,
|
@RequestParam(required = false) Integer gapDetectionToleranceSeconds,
|
||||||
|
@RequestParam(required = false) EsperSourceSelectionMode sourceSelectionMode,
|
||||||
@RequestParam(required = false) EsperUnknownTreatmentMode unknownTreatmentMode,
|
@RequestParam(required = false) EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
@RequestParam(required = false) EsperOperatingPeriodEngineMode engineMode
|
@RequestParam(required = false) EsperOperatingPeriodEngineMode engineMode
|
||||||
) {
|
) {
|
||||||
|
|
@ -90,9 +98,48 @@ public class EsperPocController {
|
||||||
significantDrivingMinutes,
|
significantDrivingMinutes,
|
||||||
mergeGapSeconds,
|
mergeGapSeconds,
|
||||||
gapDetectionToleranceSeconds,
|
gapDetectionToleranceSeconds,
|
||||||
|
sourceSelectionMode,
|
||||||
unknownTreatmentMode,
|
unknownTreatmentMode,
|
||||||
engineMode
|
engineMode
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(operatingPeriodEvaluationService.evaluate(request));
|
return ResponseEntity.ok(operatingPeriodEvaluationService.evaluate(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tachograph/dti-enrichment")
|
||||||
|
public ResponseEntity<EsperDtiEnrichmentResultDto> evaluateDtiEnrichment(
|
||||||
|
@RequestParam String tenantKey,
|
||||||
|
@RequestParam UUID driverId,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime occurredFrom,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime occurredTo,
|
||||||
|
@RequestParam(defaultValue = "24") Integer guardHours,
|
||||||
|
@RequestParam(required = false) Integer operatingSplitIdleHours,
|
||||||
|
@RequestParam(required = false) Integer significantDrivingMinutes,
|
||||||
|
@RequestParam(required = false) Integer mergeGapSeconds,
|
||||||
|
@RequestParam(required = false) Integer gapDetectionToleranceSeconds,
|
||||||
|
@RequestParam(required = false) EsperSourceSelectionMode sourceSelectionMode,
|
||||||
|
@RequestParam(required = false) EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
|
@RequestParam(required = false) EsperOperatingPeriodEngineMode engineMode,
|
||||||
|
@RequestParam(required = false) Integer vehicleEvidenceLookbackHours,
|
||||||
|
@RequestParam(required = false) Integer geoSearchWindowMinutes,
|
||||||
|
@RequestParam(required = false) Integer vicinityWindowMinutes
|
||||||
|
) {
|
||||||
|
EsperDtiEnrichmentRequest request = new EsperDtiEnrichmentRequest(
|
||||||
|
tenantKey,
|
||||||
|
driverId,
|
||||||
|
occurredFrom,
|
||||||
|
occurredTo,
|
||||||
|
guardHours,
|
||||||
|
operatingSplitIdleHours,
|
||||||
|
significantDrivingMinutes,
|
||||||
|
mergeGapSeconds,
|
||||||
|
gapDetectionToleranceSeconds,
|
||||||
|
sourceSelectionMode,
|
||||||
|
unknownTreatmentMode,
|
||||||
|
engineMode,
|
||||||
|
vehicleEvidenceLookbackHours,
|
||||||
|
geoSearchWindowMinutes,
|
||||||
|
vicinityWindowMinutes
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(dtiEnrichmentService.evaluate(request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.GeoPointDto;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record DtiBoundaryPositionDto(
|
||||||
|
OffsetDateTime evidenceAt,
|
||||||
|
GeoPointDto position,
|
||||||
|
String eventDomain,
|
||||||
|
String eventType,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
String countryFrom,
|
||||||
|
String countryTo,
|
||||||
|
String operation,
|
||||||
|
long deltaSeconds,
|
||||||
|
int confidence,
|
||||||
|
String evidenceSourceRowId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record DtiBoundaryVehicleDto(
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
|
String vehicleVin,
|
||||||
|
String resolutionSource,
|
||||||
|
int confidence,
|
||||||
|
OffsetDateTime evidenceStartedAt,
|
||||||
|
OffsetDateTime evidenceEndedAt,
|
||||||
|
List<String> evidenceSourceRowIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.GeoPointDto;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record DtiBoundaryVicinityEventDto(
|
||||||
|
OffsetDateTime occurredAt,
|
||||||
|
String eventDomain,
|
||||||
|
String eventType,
|
||||||
|
String lifecycle,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
|
GeoPointDto position,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
String countryFrom,
|
||||||
|
String countryTo,
|
||||||
|
String operation,
|
||||||
|
long deltaSeconds,
|
||||||
|
String sourceRowId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record EnrichedDtiIntervalDto(
|
||||||
|
String dtiId,
|
||||||
|
UUID driverId,
|
||||||
|
String intervalKind,
|
||||||
|
OffsetDateTime startedAt,
|
||||||
|
OffsetDateTime endedAt,
|
||||||
|
long durationSeconds,
|
||||||
|
long operatingPeriodNo,
|
||||||
|
OffsetDateTime operatingPeriodStartedAt,
|
||||||
|
String previousDrivingSourceRowId,
|
||||||
|
String nextDrivingSourceRowId,
|
||||||
|
DtiBoundaryVehicleDto beginVehicle,
|
||||||
|
DtiBoundaryVehicleDto endVehicle,
|
||||||
|
DtiBoundaryPositionDto beginPosition,
|
||||||
|
DtiBoundaryPositionDto endPosition,
|
||||||
|
List<DtiBoundaryVicinityEventDto> beginVicinityEvents,
|
||||||
|
List<DtiBoundaryVicinityEventDto> endVicinityEvents
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record EsperDtiEnrichmentRequest(
|
||||||
|
@NotBlank String tenantKey,
|
||||||
|
@NotNull UUID driverId,
|
||||||
|
@NotNull OffsetDateTime occurredFrom,
|
||||||
|
@NotNull OffsetDateTime occurredTo,
|
||||||
|
Integer guardHours,
|
||||||
|
Integer operatingSplitIdleHours,
|
||||||
|
Integer significantDrivingMinutes,
|
||||||
|
Integer mergeGapSeconds,
|
||||||
|
Integer gapDetectionToleranceSeconds,
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode,
|
||||||
|
EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
|
EsperOperatingPeriodEngineMode engineMode,
|
||||||
|
Integer vehicleEvidenceLookbackHours,
|
||||||
|
Integer geoSearchWindowMinutes,
|
||||||
|
Integer vicinityWindowMinutes
|
||||||
|
) {
|
||||||
|
public EsperDtiEnrichmentRequest {
|
||||||
|
if (occurredFrom != null && occurredTo != null && !occurredFrom.isBefore(occurredTo)) {
|
||||||
|
throw new IllegalArgumentException("occurredFrom must be before occurredTo");
|
||||||
|
}
|
||||||
|
guardHours = guardHours == null ? 24 : Math.max(0, guardHours);
|
||||||
|
operatingSplitIdleHours = operatingSplitIdleHours == null ? null : Math.max(1, operatingSplitIdleHours);
|
||||||
|
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
|
||||||
|
mergeGapSeconds = mergeGapSeconds == null ? null : Math.max(0, mergeGapSeconds);
|
||||||
|
gapDetectionToleranceSeconds = gapDetectionToleranceSeconds == null ? null : Math.max(0, gapDetectionToleranceSeconds);
|
||||||
|
vehicleEvidenceLookbackHours = vehicleEvidenceLookbackHours == null ? 24 * 30 : Math.max(1, vehicleEvidenceLookbackHours);
|
||||||
|
geoSearchWindowMinutes = geoSearchWindowMinutes == null ? 180 : Math.max(1, geoSearchWindowMinutes);
|
||||||
|
vicinityWindowMinutes = vicinityWindowMinutes == null ? 180 : Math.max(1, vicinityWindowMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record EsperDtiEnrichmentResultDto(
|
||||||
|
String tenantKey,
|
||||||
|
UUID driverId,
|
||||||
|
OffsetDateTime requestedFrom,
|
||||||
|
OffsetDateTime requestedTo,
|
||||||
|
OffsetDateTime loadedFrom,
|
||||||
|
OffsetDateTime loadedTo,
|
||||||
|
OffsetDateTime supportFrom,
|
||||||
|
OffsetDateTime supportTo,
|
||||||
|
int pureDtiCount,
|
||||||
|
int supportEventCount,
|
||||||
|
int vehicleUsageIntervalCount,
|
||||||
|
int geoSearchWindowMinutes,
|
||||||
|
int vicinityWindowMinutes,
|
||||||
|
int vehicleEvidenceLookbackHours,
|
||||||
|
List<EnrichedDtiIntervalDto> dtiIntervals,
|
||||||
|
List<String> notes
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ public record EsperOperatingPeriodRequest(
|
||||||
Integer significantDrivingMinutes,
|
Integer significantDrivingMinutes,
|
||||||
Integer mergeGapSeconds,
|
Integer mergeGapSeconds,
|
||||||
Integer gapDetectionToleranceSeconds,
|
Integer gapDetectionToleranceSeconds,
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode,
|
||||||
EsperUnknownTreatmentMode unknownTreatmentMode,
|
EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
EsperOperatingPeriodEngineMode engineMode
|
EsperOperatingPeriodEngineMode engineMode
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public record EsperOperatingPeriodResultDto(
|
||||||
int significantDrivingMinutes,
|
int significantDrivingMinutes,
|
||||||
int mergeGapSeconds,
|
int mergeGapSeconds,
|
||||||
int gapDetectionToleranceSeconds,
|
int gapDetectionToleranceSeconds,
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode,
|
||||||
EsperUnknownTreatmentMode unknownTreatmentMode,
|
EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
EsperOperatingPeriodEngineMode engineMode,
|
EsperOperatingPeriodEngineMode engineMode,
|
||||||
List<RawActivityEventDto> rawEvents,
|
List<RawActivityEventDto> rawEvents,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
public enum EsperSourceSelectionMode {
|
||||||
|
MIXED,
|
||||||
|
DRIVER_CARD_ONLY
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package at.procon.eventhub.esperpoc.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record EsperSupportEventDto(
|
||||||
|
UUID eventId,
|
||||||
|
OffsetDateTime occurredAt,
|
||||||
|
String sourceRowId,
|
||||||
|
String externalSourceEventId,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
UUID driverId,
|
||||||
|
UUID driverCardId,
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
|
String eventDomain,
|
||||||
|
String eventType,
|
||||||
|
String lifecycle,
|
||||||
|
String cardSlot,
|
||||||
|
BigDecimal latitude,
|
||||||
|
BigDecimal longitude,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
String countryFrom,
|
||||||
|
String countryTo,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package at.procon.eventhub.esperpoc.persistence;
|
||||||
|
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSupportEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class EsperPocDtiEnrichmentRepository {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
public EsperPocDtiEnrichmentRepository(JdbcTemplate jdbcTemplate) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EsperSupportEventDto> findDriverSupportEvents(
|
||||||
|
String tenantKey,
|
||||||
|
UUID driverId,
|
||||||
|
OffsetDateTime supportFrom,
|
||||||
|
OffsetDateTime supportTo,
|
||||||
|
int precedingDriverCardEventCount,
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode
|
||||||
|
) {
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
"""
|
||||||
|
with candidate as (
|
||||||
|
select
|
||||||
|
event.id,
|
||||||
|
event.occurred_at,
|
||||||
|
coalesce(
|
||||||
|
event.payload #>> '{raw,sourceRowId}',
|
||||||
|
regexp_replace(event.external_source_event_id, ':(START|END|INSERT|WITHDRAW)$', '')
|
||||||
|
) as source_row_id,
|
||||||
|
event.external_source_event_id,
|
||||||
|
source.source_kind,
|
||||||
|
coalesce(pkg.extraction_code, '') as extraction_code,
|
||||||
|
event.driver_id,
|
||||||
|
event.driver_card_id,
|
||||||
|
event.vehicle_id,
|
||||||
|
event.vehicle_registration_id,
|
||||||
|
event.event_domain,
|
||||||
|
event.event_type,
|
||||||
|
event.lifecycle,
|
||||||
|
detail.attributes ->> 'cardSlot' as card_slot,
|
||||||
|
st_y(event.position::geometry) as latitude,
|
||||||
|
st_x(event.position::geometry) as longitude,
|
||||||
|
detail.attributes ->> 'country' as country,
|
||||||
|
detail.attributes ->> 'region' as region,
|
||||||
|
detail.attributes ->> 'countryFrom' as country_from,
|
||||||
|
detail.attributes ->> 'countryTo' as country_to,
|
||||||
|
detail.attributes ->> 'operation' as operation
|
||||||
|
from eventhub.event event
|
||||||
|
join eventhub.event_source source on source.id = event.event_source_id
|
||||||
|
join eventhub.data_package pkg on pkg.id = event.data_package_id
|
||||||
|
left join lateral (
|
||||||
|
select detail.attributes
|
||||||
|
from eventhub.event_detail detail
|
||||||
|
where detail.event_occurred_at = event.occurred_at
|
||||||
|
and detail.event_id = event.id
|
||||||
|
order by detail.detail_type
|
||||||
|
limit 1
|
||||||
|
) detail on true
|
||||||
|
where pkg.tenant_key = ?
|
||||||
|
and source.provider_key = 'TACHOGRAPH'
|
||||||
|
and event.driver_id = ?
|
||||||
|
and (? <> 'DRIVER_CARD_ONLY' or source.source_kind = 'DRIVER_CARD')
|
||||||
|
and event.event_domain in ('DRIVER_CARD', 'POSITION', 'PLACE', 'BORDER_CROSSING', 'LOAD_UNLOAD', 'SPECIFIC_CONDITION', 'SPEEDING')
|
||||||
|
and event.occurred_at < ?
|
||||||
|
),
|
||||||
|
in_range as (
|
||||||
|
select * from candidate where occurred_at >= ?
|
||||||
|
),
|
||||||
|
preceding_driver_card as (
|
||||||
|
select *
|
||||||
|
from candidate
|
||||||
|
where occurred_at < ?
|
||||||
|
and event_domain = 'DRIVER_CARD'
|
||||||
|
order by occurred_at desc, id desc
|
||||||
|
limit ?
|
||||||
|
)
|
||||||
|
select *
|
||||||
|
from (
|
||||||
|
select * from in_range
|
||||||
|
union all
|
||||||
|
select * from preceding_driver_card
|
||||||
|
) result
|
||||||
|
order by occurred_at, lifecycle, event_domain, event_type, id
|
||||||
|
""",
|
||||||
|
(rs, rowNum) -> new EsperSupportEventDto(
|
||||||
|
(UUID) rs.getObject("id"),
|
||||||
|
rs.getObject("occurred_at", OffsetDateTime.class),
|
||||||
|
rs.getString("source_row_id"),
|
||||||
|
rs.getString("external_source_event_id"),
|
||||||
|
rs.getString("source_kind"),
|
||||||
|
rs.getString("extraction_code"),
|
||||||
|
(UUID) rs.getObject("driver_id"),
|
||||||
|
(UUID) rs.getObject("driver_card_id"),
|
||||||
|
(UUID) rs.getObject("vehicle_id"),
|
||||||
|
(UUID) rs.getObject("vehicle_registration_id"),
|
||||||
|
rs.getString("event_domain"),
|
||||||
|
rs.getString("event_type"),
|
||||||
|
rs.getString("lifecycle"),
|
||||||
|
rs.getString("card_slot"),
|
||||||
|
rs.getBigDecimal("latitude"),
|
||||||
|
rs.getBigDecimal("longitude"),
|
||||||
|
rs.getString("country"),
|
||||||
|
rs.getString("region"),
|
||||||
|
rs.getString("country_from"),
|
||||||
|
rs.getString("country_to"),
|
||||||
|
rs.getString("operation")
|
||||||
|
),
|
||||||
|
tenantKey,
|
||||||
|
driverId,
|
||||||
|
sourceSelectionMode == null ? EsperSourceSelectionMode.MIXED.name() : sourceSelectionMode.name(),
|
||||||
|
supportTo,
|
||||||
|
supportFrom,
|
||||||
|
supportFrom,
|
||||||
|
precedingDriverCardEventCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,649 @@
|
||||||
|
package at.procon.eventhub.esperpoc.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.GeoPointDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryPositionDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryVehicleDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryVicinityEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EnrichedDtiIntervalDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentRequest;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentResultDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.OperatingPeriodDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSupportEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.persistence.EsperPocDtiEnrichmentRepository;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EsperDtiEnrichmentService {
|
||||||
|
|
||||||
|
private static final Set<String> VEHICLE_INTERVAL_EXTRACTION_CODES = Set.of("IW_CYCLE", "CARD_VEHICLES_USED");
|
||||||
|
private static final int PRECEDING_DRIVER_CARD_EVENT_COUNT = 50;
|
||||||
|
|
||||||
|
private final EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService;
|
||||||
|
private final EsperPocDtiEnrichmentRepository enrichmentRepository;
|
||||||
|
|
||||||
|
public EsperDtiEnrichmentService(
|
||||||
|
EsperOperatingPeriodEvaluationService operatingPeriodEvaluationService,
|
||||||
|
EsperPocDtiEnrichmentRepository enrichmentRepository
|
||||||
|
) {
|
||||||
|
this.operatingPeriodEvaluationService = operatingPeriodEvaluationService;
|
||||||
|
this.enrichmentRepository = enrichmentRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EsperDtiEnrichmentResultDto evaluate(EsperDtiEnrichmentRequest request) {
|
||||||
|
EsperOperatingPeriodRequest operatingRequest = new EsperOperatingPeriodRequest(
|
||||||
|
request.tenantKey(),
|
||||||
|
request.driverId(),
|
||||||
|
request.occurredFrom(),
|
||||||
|
request.occurredTo(),
|
||||||
|
request.guardHours(),
|
||||||
|
request.operatingSplitIdleHours(),
|
||||||
|
request.significantDrivingMinutes(),
|
||||||
|
request.mergeGapSeconds(),
|
||||||
|
request.gapDetectionToleranceSeconds(),
|
||||||
|
request.sourceSelectionMode(),
|
||||||
|
request.unknownTreatmentMode(),
|
||||||
|
request.engineMode()
|
||||||
|
);
|
||||||
|
EsperOperatingPeriodResultDto pureDtiResult = operatingPeriodEvaluationService.evaluate(operatingRequest);
|
||||||
|
|
||||||
|
OffsetDateTime supportFrom = request.occurredFrom().minusHours(request.vehicleEvidenceLookbackHours());
|
||||||
|
OffsetDateTime supportTo = request.occurredTo().plusHours(Math.max(request.guardHours(), 24));
|
||||||
|
List<EsperSupportEventDto> rawSupportEvents = enrichmentRepository.findDriverSupportEvents(
|
||||||
|
request.tenantKey(),
|
||||||
|
request.driverId(),
|
||||||
|
supportFrom,
|
||||||
|
supportTo,
|
||||||
|
PRECEDING_DRIVER_CARD_EVENT_COUNT,
|
||||||
|
request.sourceSelectionMode()
|
||||||
|
);
|
||||||
|
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = mergeVehicleUsageIntervals(buildVehicleUsageIntervals(rawSupportEvents));
|
||||||
|
List<EsperSupportEventDto> supportEvents = condenseSupportEvents(rawSupportEvents);
|
||||||
|
List<PureDtiInterval> pureDtiIntervals = extractPureDtiIntervals(pureDtiResult);
|
||||||
|
List<EnrichedDtiIntervalDto> enrichedIntervals = pureDtiIntervals.stream()
|
||||||
|
.map(interval -> enrichInterval(interval, supportEvents, vehicleUsageIntervals, request))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return new EsperDtiEnrichmentResultDto(
|
||||||
|
request.tenantKey(),
|
||||||
|
request.driverId(),
|
||||||
|
pureDtiResult.requestedFrom(),
|
||||||
|
pureDtiResult.requestedTo(),
|
||||||
|
pureDtiResult.loadedFrom(),
|
||||||
|
pureDtiResult.loadedTo(),
|
||||||
|
supportFrom,
|
||||||
|
supportTo,
|
||||||
|
pureDtiIntervals.size(),
|
||||||
|
supportEvents.size(),
|
||||||
|
vehicleUsageIntervals.size(),
|
||||||
|
request.geoSearchWindowMinutes(),
|
||||||
|
request.vicinityWindowMinutes(),
|
||||||
|
request.vehicleEvidenceLookbackHours(),
|
||||||
|
enrichedIntervals,
|
||||||
|
notes(request)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnrichedDtiIntervalDto enrichInterval(
|
||||||
|
PureDtiInterval interval,
|
||||||
|
List<EsperSupportEventDto> supportEvents,
|
||||||
|
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||||
|
EsperDtiEnrichmentRequest request
|
||||||
|
) {
|
||||||
|
DtiBoundaryVehicleDto beginVehicle = resolveBoundaryVehicle(interval.startedAt(), vehicleUsageIntervals);
|
||||||
|
DtiBoundaryVehicleDto endVehicle = resolveBoundaryVehicle(interval.endedAt(), vehicleUsageIntervals);
|
||||||
|
DtiBoundaryPositionDto beginPosition = resolveBoundaryPosition(
|
||||||
|
interval.startedAt(),
|
||||||
|
beginVehicle,
|
||||||
|
supportEvents,
|
||||||
|
request.geoSearchWindowMinutes()
|
||||||
|
);
|
||||||
|
DtiBoundaryPositionDto endPosition = resolveBoundaryPosition(
|
||||||
|
interval.endedAt(),
|
||||||
|
endVehicle,
|
||||||
|
supportEvents,
|
||||||
|
request.geoSearchWindowMinutes()
|
||||||
|
);
|
||||||
|
List<DtiBoundaryVicinityEventDto> beginVicinity = resolveBoundaryVicinityEvents(
|
||||||
|
interval.startedAt(),
|
||||||
|
supportEvents,
|
||||||
|
request.vicinityWindowMinutes()
|
||||||
|
);
|
||||||
|
List<DtiBoundaryVicinityEventDto> endVicinity = resolveBoundaryVicinityEvents(
|
||||||
|
interval.endedAt(),
|
||||||
|
supportEvents,
|
||||||
|
request.vicinityWindowMinutes()
|
||||||
|
);
|
||||||
|
return new EnrichedDtiIntervalDto(
|
||||||
|
interval.dtiId(),
|
||||||
|
interval.driverId(),
|
||||||
|
interval.intervalKind(),
|
||||||
|
interval.startedAt(),
|
||||||
|
interval.endedAt(),
|
||||||
|
interval.durationSeconds(),
|
||||||
|
interval.operatingPeriodNo(),
|
||||||
|
interval.operatingPeriodStartedAt(),
|
||||||
|
interval.previousDrivingSourceRowId(),
|
||||||
|
interval.nextDrivingSourceRowId(),
|
||||||
|
beginVehicle,
|
||||||
|
endVehicle,
|
||||||
|
beginPosition,
|
||||||
|
endPosition,
|
||||||
|
beginVicinity,
|
||||||
|
endVicinity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PureDtiInterval> extractPureDtiIntervals(EsperOperatingPeriodResultDto result) {
|
||||||
|
if (result == null || result.operatingPeriods() == null || result.operatingPeriods().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<PureDtiInterval> intervals = new ArrayList<>();
|
||||||
|
for (OperatingPeriodDto operatingPeriod : result.operatingPeriods()) {
|
||||||
|
if (operatingPeriod.drivingTimeInterruptionEvaluation() == null
|
||||||
|
|| operatingPeriod.drivingTimeInterruptionEvaluation().interruptionsBetweenSignificantDrivingPeriods() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (operatingPeriod.drivingTimeInterruptionEvaluation().departureAt() != null
|
||||||
|
&& operatingPeriod.startedAt().isBefore(operatingPeriod.drivingTimeInterruptionEvaluation().departureAt())) {
|
||||||
|
intervals.add(new PureDtiInterval(
|
||||||
|
"DTI-PRE-" + operatingPeriod.operatingPeriodNo() + "-" + operatingPeriod.startedAt().toInstant().toEpochMilli(),
|
||||||
|
result.driverId(),
|
||||||
|
"BEFORE_FIRST_SIGNIFICANT_DRIVING",
|
||||||
|
operatingPeriod.startedAt(),
|
||||||
|
operatingPeriod.drivingTimeInterruptionEvaluation().departureAt(),
|
||||||
|
Duration.between(operatingPeriod.startedAt(), operatingPeriod.drivingTimeInterruptionEvaluation().departureAt()).getSeconds(),
|
||||||
|
operatingPeriod.operatingPeriodNo(),
|
||||||
|
operatingPeriod.startedAt(),
|
||||||
|
null,
|
||||||
|
operatingPeriod.drivingTimeInterruptionEvaluation().firstSignificantDrivingPeriod() == null
|
||||||
|
? null
|
||||||
|
: operatingPeriod.drivingTimeInterruptionEvaluation().firstSignificantDrivingPeriod().sourceRowId()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
operatingPeriod.drivingTimeInterruptionEvaluation().interruptionsBetweenSignificantDrivingPeriods()
|
||||||
|
.forEach(interruption -> intervals.add(new PureDtiInterval(
|
||||||
|
"DTI-" + operatingPeriod.operatingPeriodNo() + "-" + interruption.from().toInstant().toEpochMilli(),
|
||||||
|
result.driverId(),
|
||||||
|
"BETWEEN_SIGNIFICANT_DRIVING",
|
||||||
|
interruption.from(),
|
||||||
|
interruption.to(),
|
||||||
|
interruption.durationSeconds(),
|
||||||
|
operatingPeriod.operatingPeriodNo(),
|
||||||
|
operatingPeriod.startedAt(),
|
||||||
|
interruption.previousDrivingSourceRowId(),
|
||||||
|
interruption.nextDrivingSourceRowId()
|
||||||
|
)));
|
||||||
|
if (operatingPeriod.drivingTimeInterruptionEvaluation().arrivalAt() != null
|
||||||
|
&& operatingPeriod.drivingTimeInterruptionEvaluation().arrivalAt().isBefore(operatingPeriod.endedAt())) {
|
||||||
|
intervals.add(new PureDtiInterval(
|
||||||
|
"DTI-POST-" + operatingPeriod.operatingPeriodNo() + "-" + operatingPeriod.drivingTimeInterruptionEvaluation().arrivalAt().toInstant().toEpochMilli(),
|
||||||
|
result.driverId(),
|
||||||
|
"AFTER_LAST_SIGNIFICANT_DRIVING",
|
||||||
|
operatingPeriod.drivingTimeInterruptionEvaluation().arrivalAt(),
|
||||||
|
operatingPeriod.endedAt(),
|
||||||
|
Duration.between(operatingPeriod.drivingTimeInterruptionEvaluation().arrivalAt(), operatingPeriod.endedAt()).getSeconds(),
|
||||||
|
operatingPeriod.operatingPeriodNo(),
|
||||||
|
operatingPeriod.startedAt(),
|
||||||
|
operatingPeriod.drivingTimeInterruptionEvaluation().lastSignificantDrivingPeriod() == null
|
||||||
|
? null
|
||||||
|
: operatingPeriod.drivingTimeInterruptionEvaluation().lastSignificantDrivingPeriod().sourceRowId(),
|
||||||
|
null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResolvedVehicleUsageInterval> buildVehicleUsageIntervals(List<EsperSupportEventDto> supportEvents) {
|
||||||
|
record VehicleIntervalSeed(
|
||||||
|
EsperSupportEventDto insertEvent,
|
||||||
|
EsperSupportEventDto withdrawEvent
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
java.util.Map<String, VehicleIntervalSeed> bySourceRow = new java.util.LinkedHashMap<>();
|
||||||
|
supportEvents.stream()
|
||||||
|
.filter(event -> "DRIVER_CARD".equals(event.eventDomain()))
|
||||||
|
.filter(event -> VEHICLE_INTERVAL_EXTRACTION_CODES.contains(event.extractionCode()))
|
||||||
|
.filter(event -> "CARD_INSERTED".equals(event.eventType()) || "CARD_WITHDRAWN".equals(event.eventType()))
|
||||||
|
.filter(event -> event.sourceRowId() != null)
|
||||||
|
.forEach(event -> {
|
||||||
|
String key = event.extractionCode() + ":" + event.sourceRowId();
|
||||||
|
VehicleIntervalSeed current = bySourceRow.get(key);
|
||||||
|
EsperSupportEventDto insert = current == null ? null : current.insertEvent();
|
||||||
|
EsperSupportEventDto withdraw = current == null ? null : current.withdrawEvent();
|
||||||
|
if ("CARD_INSERTED".equals(event.eventType())) {
|
||||||
|
if (insert == null || event.occurredAt().isBefore(insert.occurredAt())) {
|
||||||
|
insert = event;
|
||||||
|
}
|
||||||
|
} else if (withdraw == null || event.occurredAt().isAfter(withdraw.occurredAt())) {
|
||||||
|
withdraw = event;
|
||||||
|
}
|
||||||
|
bySourceRow.put(key, new VehicleIntervalSeed(insert, withdraw));
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
|
||||||
|
for (VehicleIntervalSeed seed : bySourceRow.values()) {
|
||||||
|
EsperSupportEventDto anchor = seed.insertEvent() != null ? seed.insertEvent() : seed.withdrawEvent();
|
||||||
|
if (anchor == null || anchor.vehicleId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
OffsetDateTime startedAt = seed.insertEvent() == null ? null : seed.insertEvent().occurredAt();
|
||||||
|
OffsetDateTime endedAt = seed.withdrawEvent() == null ? null : seed.withdrawEvent().occurredAt();
|
||||||
|
if (startedAt == null && endedAt == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(new ResolvedVehicleUsageInterval(
|
||||||
|
anchor.driverId(),
|
||||||
|
anchor.driverCardId(),
|
||||||
|
anchor.vehicleId(),
|
||||||
|
anchor.vehicleRegistrationId(),
|
||||||
|
anchor.extractionCode(),
|
||||||
|
startedAt,
|
||||||
|
endedAt,
|
||||||
|
sourcePriority(anchor.extractionCode()),
|
||||||
|
sourceConfidence(anchor.extractionCode()),
|
||||||
|
collectSourceRowIds(seed.insertEvent(), seed.withdrawEvent())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result.stream()
|
||||||
|
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::startedAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||||
|
.thenComparing(ResolvedVehicleUsageInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(List<ResolvedVehicleUsageInterval> intervals) {
|
||||||
|
if (intervals.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<ResolvedVehicleUsageInterval> sorted = intervals.stream()
|
||||||
|
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::startedAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||||
|
.thenComparing(ResolvedVehicleUsageInterval::endedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||||
|
.toList();
|
||||||
|
List<ResolvedVehicleUsageInterval> merged = new ArrayList<>();
|
||||||
|
ResolvedVehicleUsageInterval current = null;
|
||||||
|
for (ResolvedVehicleUsageInterval next : sorted) {
|
||||||
|
if (current == null) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (canMerge(current, next)) {
|
||||||
|
current = current.merge(next);
|
||||||
|
} else {
|
||||||
|
merged.add(current);
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current != null) {
|
||||||
|
merged.add(current);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EsperSupportEventDto> condenseSupportEvents(List<EsperSupportEventDto> supportEvents) {
|
||||||
|
if (supportEvents == null || supportEvents.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
record VehicleIntervalSeed(
|
||||||
|
EsperSupportEventDto insertEvent,
|
||||||
|
EsperSupportEventDto withdrawEvent
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
java.util.Map<String, VehicleIntervalSeed> seedsByKey = new java.util.LinkedHashMap<>();
|
||||||
|
supportEvents.stream()
|
||||||
|
.filter(event -> "DRIVER_CARD".equals(event.eventDomain()))
|
||||||
|
.filter(event -> "CARD_VEHICLES_USED".equals(event.extractionCode()))
|
||||||
|
.filter(event -> "CARD_INSERTED".equals(event.eventType()) || "CARD_WITHDRAWN".equals(event.eventType()))
|
||||||
|
.filter(event -> event.sourceRowId() != null)
|
||||||
|
.forEach(event -> {
|
||||||
|
String key = event.extractionCode() + ":" + event.sourceRowId();
|
||||||
|
VehicleIntervalSeed current = seedsByKey.get(key);
|
||||||
|
EsperSupportEventDto insert = current == null ? null : current.insertEvent();
|
||||||
|
EsperSupportEventDto withdraw = current == null ? null : current.withdrawEvent();
|
||||||
|
if ("CARD_INSERTED".equals(event.eventType())) {
|
||||||
|
if (insert == null || event.occurredAt().isBefore(insert.occurredAt())) {
|
||||||
|
insert = event;
|
||||||
|
}
|
||||||
|
} else if (withdraw == null || event.occurredAt().isAfter(withdraw.occurredAt())) {
|
||||||
|
withdraw = event;
|
||||||
|
}
|
||||||
|
seedsByKey.put(key, new VehicleIntervalSeed(insert, withdraw));
|
||||||
|
});
|
||||||
|
|
||||||
|
List<ResolvedVehicleUsageInterval> mergedCardVehicleIntervals = mergeVehicleUsageIntervals(
|
||||||
|
buildVehicleUsageIntervals(supportEvents).stream()
|
||||||
|
.filter(interval -> "CARD_VEHICLES_USED".equals(interval.authoritativeSource()))
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
Set<UUID> keptDriverCardEventIds = new LinkedHashSet<>();
|
||||||
|
for (ResolvedVehicleUsageInterval interval : mergedCardVehicleIntervals) {
|
||||||
|
EsperSupportEventDto earliestInsert = null;
|
||||||
|
EsperSupportEventDto latestWithdraw = null;
|
||||||
|
for (String sourceRowId : interval.sourceRowIds()) {
|
||||||
|
VehicleIntervalSeed seed = seedsByKey.get("CARD_VEHICLES_USED:" + sourceRowId);
|
||||||
|
if (seed == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seed.insertEvent() != null
|
||||||
|
&& (earliestInsert == null || seed.insertEvent().occurredAt().isBefore(earliestInsert.occurredAt()))) {
|
||||||
|
earliestInsert = seed.insertEvent();
|
||||||
|
}
|
||||||
|
if (seed.withdrawEvent() != null
|
||||||
|
&& (latestWithdraw == null || seed.withdrawEvent().occurredAt().isAfter(latestWithdraw.occurredAt()))) {
|
||||||
|
latestWithdraw = seed.withdrawEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (earliestInsert != null) {
|
||||||
|
keptDriverCardEventIds.add(earliestInsert.eventId());
|
||||||
|
}
|
||||||
|
if (latestWithdraw != null) {
|
||||||
|
keptDriverCardEventIds.add(latestWithdraw.eventId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportEvents.stream()
|
||||||
|
.filter(event -> !("DRIVER_CARD".equals(event.eventDomain())
|
||||||
|
&& "CARD_VEHICLES_USED".equals(event.extractionCode())
|
||||||
|
&& !keptDriverCardEventIds.contains(event.eventId())))
|
||||||
|
.sorted(Comparator.comparing(EsperSupportEventDto::occurredAt)
|
||||||
|
.thenComparing(EsperSupportEventDto::lifecycle, Comparator.nullsLast(String::compareTo))
|
||||||
|
.thenComparing(EsperSupportEventDto::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||||
|
.thenComparing(EsperSupportEventDto::eventType, Comparator.nullsLast(String::compareTo)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
DtiBoundaryVehicleDto resolveBoundaryVehicle(
|
||||||
|
OffsetDateTime boundary,
|
||||||
|
List<ResolvedVehicleUsageInterval> intervals
|
||||||
|
) {
|
||||||
|
return intervals.stream()
|
||||||
|
.map(interval -> intervalCandidate(boundary, interval))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.sorted(Comparator.comparing(VehicleBoundaryCandidate::score).reversed()
|
||||||
|
.thenComparing(VehicleBoundaryCandidate::deltaSeconds)
|
||||||
|
.thenComparing(candidate -> candidate.interval.startedAt(), Comparator.nullsLast(Comparator.reverseOrder())))
|
||||||
|
.map(candidate -> new DtiBoundaryVehicleDto(
|
||||||
|
candidate.interval.vehicleId(),
|
||||||
|
candidate.interval.vehicleRegistrationId(),
|
||||||
|
null,
|
||||||
|
candidate.interval.authoritativeSource(),
|
||||||
|
candidate.interval.confidence(),
|
||||||
|
candidate.interval.startedAt(),
|
||||||
|
candidate.interval.endedAt(),
|
||||||
|
candidate.interval.sourceRowIds()
|
||||||
|
))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DtiBoundaryPositionDto resolveBoundaryPosition(
|
||||||
|
OffsetDateTime boundary,
|
||||||
|
DtiBoundaryVehicleDto boundaryVehicle,
|
||||||
|
List<EsperSupportEventDto> supportEvents,
|
||||||
|
int searchWindowMinutes
|
||||||
|
) {
|
||||||
|
long maxDeltaSeconds = Duration.ofMinutes(searchWindowMinutes).getSeconds();
|
||||||
|
return supportEvents.stream()
|
||||||
|
.filter(this::hasPosition)
|
||||||
|
.filter(event -> Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()) <= maxDeltaSeconds)
|
||||||
|
.sorted(Comparator
|
||||||
|
.comparing((EsperSupportEventDto event) -> vehicleMatch(event, boundaryVehicle)).reversed()
|
||||||
|
.thenComparing(event -> Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()))
|
||||||
|
.thenComparing((EsperSupportEventDto event) -> geoDomainPriority(event.eventDomain()), Comparator.reverseOrder())
|
||||||
|
.thenComparing(EsperSupportEventDto::occurredAt, Comparator.reverseOrder()))
|
||||||
|
.map(event -> toBoundaryPosition(boundary, boundaryVehicle, event))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DtiBoundaryVicinityEventDto> resolveBoundaryVicinityEvents(
|
||||||
|
OffsetDateTime boundary,
|
||||||
|
List<EsperSupportEventDto> supportEvents,
|
||||||
|
int vicinityWindowMinutes
|
||||||
|
) {
|
||||||
|
long maxDeltaSeconds = Duration.ofMinutes(vicinityWindowMinutes).getSeconds();
|
||||||
|
return supportEvents.stream()
|
||||||
|
.filter(event -> Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()) <= maxDeltaSeconds)
|
||||||
|
.sorted(Comparator
|
||||||
|
.comparing((EsperSupportEventDto event) -> Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()))
|
||||||
|
.thenComparing((EsperSupportEventDto event) -> supportEventPriority(event.eventDomain()), Comparator.reverseOrder())
|
||||||
|
.thenComparing(EsperSupportEventDto::occurredAt, Comparator.reverseOrder()))
|
||||||
|
.limit(20)
|
||||||
|
.map(event -> new DtiBoundaryVicinityEventDto(
|
||||||
|
event.occurredAt(),
|
||||||
|
event.eventDomain(),
|
||||||
|
event.eventType(),
|
||||||
|
event.lifecycle(),
|
||||||
|
event.sourceKind(),
|
||||||
|
event.extractionCode(),
|
||||||
|
event.vehicleId(),
|
||||||
|
event.vehicleRegistrationId(),
|
||||||
|
toGeoPoint(event),
|
||||||
|
event.country(),
|
||||||
|
event.region(),
|
||||||
|
event.countryFrom(),
|
||||||
|
event.countryTo(),
|
||||||
|
event.operation(),
|
||||||
|
Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()),
|
||||||
|
event.sourceRowId()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VehicleBoundaryCandidate intervalCandidate(OffsetDateTime boundary, ResolvedVehicleUsageInterval interval) {
|
||||||
|
if (interval.startedAt() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean covers = !interval.startedAt().isAfter(boundary) && (interval.endedAt() == null || interval.endedAt().isAfter(boundary) || interval.endedAt().isEqual(boundary));
|
||||||
|
long deltaSeconds;
|
||||||
|
int baseScore;
|
||||||
|
if (covers) {
|
||||||
|
deltaSeconds = 0;
|
||||||
|
baseScore = interval.priority() + 200;
|
||||||
|
} else if (interval.endedAt() != null && interval.endedAt().isBefore(boundary)) {
|
||||||
|
deltaSeconds = Duration.between(interval.endedAt(), boundary).getSeconds();
|
||||||
|
baseScore = interval.priority();
|
||||||
|
} else {
|
||||||
|
deltaSeconds = Math.abs(Duration.between(boundary, interval.startedAt()).getSeconds());
|
||||||
|
baseScore = interval.priority() - 25;
|
||||||
|
}
|
||||||
|
if (deltaSeconds > Duration.ofHours(24).getSeconds()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new VehicleBoundaryCandidate(interval, baseScore, deltaSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canMerge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||||
|
if (!Objects.equals(left.driverId(), right.driverId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(left.vehicleId(), right.vehicleId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean midnightCardVehiclesUsedContinuation =
|
||||||
|
"CARD_VEHICLES_USED".equals(left.authoritativeSource())
|
||||||
|
&& "CARD_VEHICLES_USED".equals(right.authoritativeSource())
|
||||||
|
&& left.endedAt() != null
|
||||||
|
&& right.startedAt() != null
|
||||||
|
&& Duration.between(left.endedAt(), right.startedAt()).getSeconds() == 1;
|
||||||
|
if (!Objects.equals(left.vehicleRegistrationId(), right.vehicleRegistrationId())
|
||||||
|
&& !midnightCardVehiclesUsedContinuation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (left.endedAt() == null || right.startedAt() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !right.startedAt().isAfter(left.endedAt().plusSeconds(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPosition(EsperSupportEventDto event) {
|
||||||
|
return event.latitude() != null && event.longitude() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean vehicleMatch(EsperSupportEventDto event, DtiBoundaryVehicleDto boundaryVehicle) {
|
||||||
|
if (boundaryVehicle == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Objects.equals(event.vehicleId(), boundaryVehicle.vehicleId())
|
||||||
|
|| (boundaryVehicle.vehicleRegistrationId() != null
|
||||||
|
&& Objects.equals(event.vehicleRegistrationId(), boundaryVehicle.vehicleRegistrationId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DtiBoundaryPositionDto toBoundaryPosition(
|
||||||
|
OffsetDateTime boundary,
|
||||||
|
DtiBoundaryVehicleDto boundaryVehicle,
|
||||||
|
EsperSupportEventDto event
|
||||||
|
) {
|
||||||
|
int confidence = geoDomainPriority(event.eventDomain()) + (vehicleMatch(event, boundaryVehicle) ? 100 : 0);
|
||||||
|
return new DtiBoundaryPositionDto(
|
||||||
|
event.occurredAt(),
|
||||||
|
toGeoPoint(event),
|
||||||
|
event.eventDomain(),
|
||||||
|
event.eventType(),
|
||||||
|
event.sourceKind(),
|
||||||
|
event.extractionCode(),
|
||||||
|
event.vehicleId(),
|
||||||
|
event.vehicleRegistrationId(),
|
||||||
|
event.country(),
|
||||||
|
event.region(),
|
||||||
|
event.countryFrom(),
|
||||||
|
event.countryTo(),
|
||||||
|
event.operation(),
|
||||||
|
Math.abs(Duration.between(boundary, event.occurredAt()).getSeconds()),
|
||||||
|
confidence,
|
||||||
|
event.sourceRowId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeoPointDto toGeoPoint(EsperSupportEventDto event) {
|
||||||
|
if (!hasPosition(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new GeoPointDto(event.latitude(), event.longitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int geoDomainPriority(String eventDomain) {
|
||||||
|
return switch (eventDomain) {
|
||||||
|
case "POSITION" -> 400;
|
||||||
|
case "PLACE" -> 350;
|
||||||
|
case "BORDER_CROSSING" -> 300;
|
||||||
|
case "LOAD_UNLOAD" -> 250;
|
||||||
|
case "SPECIFIC_CONDITION" -> 200;
|
||||||
|
default -> 100;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int supportEventPriority(String eventDomain) {
|
||||||
|
return switch (eventDomain) {
|
||||||
|
case "DRIVER_CARD" -> 500;
|
||||||
|
case "POSITION" -> 450;
|
||||||
|
case "PLACE" -> 400;
|
||||||
|
case "BORDER_CROSSING" -> 350;
|
||||||
|
case "LOAD_UNLOAD" -> 300;
|
||||||
|
case "SPECIFIC_CONDITION" -> 250;
|
||||||
|
case "SPEEDING" -> 200;
|
||||||
|
default -> 100;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int sourcePriority(String extractionCode) {
|
||||||
|
return "IW_CYCLE".equals(extractionCode) ? 1000 : "CARD_VEHICLES_USED".equals(extractionCode) ? 900 : 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int sourceConfidence(String extractionCode) {
|
||||||
|
return "IW_CYCLE".equals(extractionCode) ? 100 : "CARD_VEHICLES_USED".equals(extractionCode) ? 90 : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> collectSourceRowIds(EsperSupportEventDto first, EsperSupportEventDto second) {
|
||||||
|
Set<String> ids = new LinkedHashSet<>();
|
||||||
|
if (first != null && first.sourceRowId() != null) {
|
||||||
|
ids.add(first.sourceRowId());
|
||||||
|
}
|
||||||
|
if (second != null && second.sourceRowId() != null) {
|
||||||
|
ids.add(second.sourceRowId());
|
||||||
|
}
|
||||||
|
return List.copyOf(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> notes(EsperDtiEnrichmentRequest request) {
|
||||||
|
return List.of(
|
||||||
|
"Pure DTI intervals come from operating-period driving interruption evaluation, including before-first and after-last significant driving intervals.",
|
||||||
|
"Vehicle evidence prefers IW_CYCLE over CARD_VEHICLES_USED when both describe the same time span.",
|
||||||
|
"Boundary geo selection prefers POSITION, then PLACE, then BORDER_CROSSING and LOAD_UNLOAD.",
|
||||||
|
"Vicinity events include non-activity tachograph events around each DTI boundary.",
|
||||||
|
"Vehicle lookback window is " + request.vehicleEvidenceLookbackHours() + " hours."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record ResolvedVehicleUsageInterval(
|
||||||
|
UUID driverId,
|
||||||
|
UUID driverCardId,
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
|
String authoritativeSource,
|
||||||
|
OffsetDateTime startedAt,
|
||||||
|
OffsetDateTime endedAt,
|
||||||
|
int priority,
|
||||||
|
int confidence,
|
||||||
|
List<String> sourceRowIds
|
||||||
|
) {
|
||||||
|
ResolvedVehicleUsageInterval merge(ResolvedVehicleUsageInterval other) {
|
||||||
|
OffsetDateTime mergedStart = startedAt == null ? other.startedAt : other.startedAt == null ? startedAt
|
||||||
|
: startedAt.isBefore(other.startedAt) ? startedAt : other.startedAt;
|
||||||
|
OffsetDateTime mergedEnd;
|
||||||
|
if (endedAt == null || other.endedAt == null) {
|
||||||
|
mergedEnd = null;
|
||||||
|
} else {
|
||||||
|
mergedEnd = endedAt.isAfter(other.endedAt) ? endedAt : other.endedAt;
|
||||||
|
}
|
||||||
|
String source = priority >= other.priority ? authoritativeSource : other.authoritativeSource;
|
||||||
|
int mergedPriority = Math.max(priority, other.priority);
|
||||||
|
int mergedConfidence = Math.max(confidence, other.confidence);
|
||||||
|
LinkedHashSet<String> mergedIds = new LinkedHashSet<>(sourceRowIds);
|
||||||
|
mergedIds.addAll(other.sourceRowIds);
|
||||||
|
return new ResolvedVehicleUsageInterval(
|
||||||
|
driverId,
|
||||||
|
driverCardId != null ? driverCardId : other.driverCardId,
|
||||||
|
vehicleId,
|
||||||
|
vehicleRegistrationId != null ? vehicleRegistrationId : other.vehicleRegistrationId,
|
||||||
|
source,
|
||||||
|
mergedStart,
|
||||||
|
mergedEnd,
|
||||||
|
mergedPriority,
|
||||||
|
mergedConfidence,
|
||||||
|
List.copyOf(mergedIds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record VehicleBoundaryCandidate(
|
||||||
|
ResolvedVehicleUsageInterval interval,
|
||||||
|
int score,
|
||||||
|
long deltaSeconds
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record PureDtiInterval(
|
||||||
|
String dtiId,
|
||||||
|
UUID driverId,
|
||||||
|
String intervalKind,
|
||||||
|
OffsetDateTime startedAt,
|
||||||
|
OffsetDateTime endedAt,
|
||||||
|
long durationSeconds,
|
||||||
|
long operatingPeriodNo,
|
||||||
|
OffsetDateTime operatingPeriodStartedAt,
|
||||||
|
String previousDrivingSourceRowId,
|
||||||
|
String nextDrivingSourceRowId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import at.procon.eventhub.esperpoc.dto.DrivingInterruptionDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
|
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
|
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
|
||||||
|
|
@ -68,6 +69,7 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
Duration significantDrivingThreshold = Duration.ofMinutes(resolveSignificantDrivingMinutes(request));
|
Duration significantDrivingThreshold = Duration.ofMinutes(resolveSignificantDrivingMinutes(request));
|
||||||
Duration mergeGapTolerance = Duration.ofSeconds(resolveMergeGapSeconds(request));
|
Duration mergeGapTolerance = Duration.ofSeconds(resolveMergeGapSeconds(request));
|
||||||
Duration gapDetectionTolerance = Duration.ofSeconds(resolveGapDetectionToleranceSeconds(request));
|
Duration gapDetectionTolerance = Duration.ofSeconds(resolveGapDetectionToleranceSeconds(request));
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode = resolveSourceSelectionMode(request);
|
||||||
EsperUnknownTreatmentMode unknownTreatmentMode = resolveUnknownTreatmentMode(request);
|
EsperUnknownTreatmentMode unknownTreatmentMode = resolveUnknownTreatmentMode(request);
|
||||||
EsperOperatingPeriodEngineMode engineMode = resolveEngineMode(request);
|
EsperOperatingPeriodEngineMode engineMode = resolveEngineMode(request);
|
||||||
|
|
||||||
|
|
@ -82,9 +84,14 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
List<RawActivityEventDto> driverCardRawEvents = rawEvents.stream()
|
List<RawActivityEventDto> driverCardRawEvents = rawEvents.stream()
|
||||||
.filter(event -> "DRIVER_CARD".equals(event.sourceKind()))
|
.filter(event -> "DRIVER_CARD".equals(event.sourceKind()))
|
||||||
.toList();
|
.toList();
|
||||||
List<RawActivityEventDto> vehicleUnitRawEvents = rawEvents.stream()
|
List<RawActivityEventDto> vehicleUnitRawEvents = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
|
||||||
|
? List.of()
|
||||||
|
: rawEvents.stream()
|
||||||
.filter(event -> "VEHICLE_UNIT".equals(event.sourceKind()))
|
.filter(event -> "VEHICLE_UNIT".equals(event.sourceKind()))
|
||||||
.toList();
|
.toList();
|
||||||
|
List<RawActivityEventDto> selectedRawEvents = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
|
||||||
|
? driverCardRawEvents
|
||||||
|
: rawEvents;
|
||||||
|
|
||||||
long cardIntervalsStartedNanos = System.nanoTime();
|
long cardIntervalsStartedNanos = System.nanoTime();
|
||||||
List<ActivityIntervalDto> driverCardRawIntervals = activityEngine.buildIntervals(driverCardRawEvents);
|
List<ActivityIntervalDto> driverCardRawIntervals = activityEngine.buildIntervals(driverCardRawEvents);
|
||||||
|
|
@ -94,7 +101,9 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
long vuIntervalsElapsedMs = elapsedMillis(vuIntervalsStartedNanos);
|
long vuIntervalsElapsedMs = elapsedMillis(vuIntervalsStartedNanos);
|
||||||
|
|
||||||
long vuGapFillStartedNanos = System.nanoTime();
|
long vuGapFillStartedNanos = System.nanoTime();
|
||||||
List<ActivityIntervalDto> resolvedKnownLoadedIntervals = resolveVuFillGaps(driverCardRawIntervals, vehicleUnitRawIntervals);
|
List<ActivityIntervalDto> resolvedKnownLoadedIntervals = sourceSelectionMode == EsperSourceSelectionMode.DRIVER_CARD_ONLY
|
||||||
|
? driverCardRawIntervals
|
||||||
|
: resolveVuFillGaps(driverCardRawIntervals, vehicleUnitRawIntervals);
|
||||||
long vuGapFillElapsedMs = elapsedMillis(vuGapFillStartedNanos);
|
long vuGapFillElapsedMs = elapsedMillis(vuGapFillStartedNanos);
|
||||||
|
|
||||||
long unknownGapStartedNanos = System.nanoTime();
|
long unknownGapStartedNanos = System.nanoTime();
|
||||||
|
|
@ -153,16 +162,17 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
);
|
);
|
||||||
long totalElapsedMs = elapsedMillis(startedNanos);
|
long totalElapsedMs = elapsedMillis(startedNanos);
|
||||||
|
|
||||||
log.info("Esper operating-period evaluation tenant={} driverId={} requestedFrom={} requestedTo={} loadedFrom={} loadedTo={} unknownMode={} engineMode={} rawEvents={} cardRawEvents={} vuRawEvents={} cardIntervals={} vuIntervals={} resolvedKnownIntervals={} evaluationIntervals={} periodizedIntervals={} mergedIntervals={} nonDrivingIntervals={} operatingPeriods={} timingsMs={{dbRetrieve={}, cardIntervalEsper={}, vuIntervalEsper={}, vuGapFill={}, synthUnknown={}, periodizeEsper={}, merge={}, nonDriving={}, total={}}}",
|
log.info("Esper operating-period evaluation tenant={} driverId={} requestedFrom={} requestedTo={} loadedFrom={} loadedTo={} sourceSelectionMode={} unknownMode={} engineMode={} rawEvents={} cardRawEvents={} vuRawEvents={} cardIntervals={} vuIntervals={} resolvedKnownIntervals={} evaluationIntervals={} periodizedIntervals={} mergedIntervals={} nonDrivingIntervals={} operatingPeriods={} timingsMs={{dbRetrieve={}, cardIntervalEsper={}, vuIntervalEsper={}, vuGapFill={}, synthUnknown={}, periodizeEsper={}, merge={}, nonDriving={}, total={}}}",
|
||||||
request.tenantKey(),
|
request.tenantKey(),
|
||||||
request.driverId(),
|
request.driverId(),
|
||||||
requestedFrom,
|
requestedFrom,
|
||||||
requestedTo,
|
requestedTo,
|
||||||
loadedFrom,
|
loadedFrom,
|
||||||
loadedTo,
|
loadedTo,
|
||||||
|
sourceSelectionMode,
|
||||||
unknownTreatmentMode,
|
unknownTreatmentMode,
|
||||||
engineMode,
|
engineMode,
|
||||||
rawEvents.size(),
|
selectedRawEvents.size(),
|
||||||
driverCardRawEvents.size(),
|
driverCardRawEvents.size(),
|
||||||
vehicleUnitRawEvents.size(),
|
vehicleUnitRawEvents.size(),
|
||||||
driverCardRawIntervals.size(),
|
driverCardRawIntervals.size(),
|
||||||
|
|
@ -190,7 +200,7 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
requestedTo,
|
requestedTo,
|
||||||
loadedFrom,
|
loadedFrom,
|
||||||
loadedTo,
|
loadedTo,
|
||||||
rawEvents.size(),
|
selectedRawEvents.size(),
|
||||||
driverCardRawEvents.size(),
|
driverCardRawEvents.size(),
|
||||||
vehicleUnitRawEvents.size(),
|
vehicleUnitRawEvents.size(),
|
||||||
driverCardRawIntervals.size(),
|
driverCardRawIntervals.size(),
|
||||||
|
|
@ -205,9 +215,10 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
resolveSignificantDrivingMinutes(request),
|
resolveSignificantDrivingMinutes(request),
|
||||||
resolveMergeGapSeconds(request),
|
resolveMergeGapSeconds(request),
|
||||||
resolveGapDetectionToleranceSeconds(request),
|
resolveGapDetectionToleranceSeconds(request),
|
||||||
|
sourceSelectionMode,
|
||||||
unknownTreatmentMode,
|
unknownTreatmentMode,
|
||||||
engineMode,
|
engineMode,
|
||||||
rawEvents,
|
selectedRawEvents,
|
||||||
resolvedKnownLoadedIntervals,
|
resolvedKnownLoadedIntervals,
|
||||||
evaluationLoadedIntervals,
|
evaluationLoadedIntervals,
|
||||||
periodizedIntervals,
|
periodizedIntervals,
|
||||||
|
|
@ -219,7 +230,8 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
unknownTreatmentMode,
|
unknownTreatmentMode,
|
||||||
resolveOperatingSplitIdleHours(request),
|
resolveOperatingSplitIdleHours(request),
|
||||||
resolveSignificantDrivingMinutes(request),
|
resolveSignificantDrivingMinutes(request),
|
||||||
resolveGapDetectionToleranceSeconds(request)
|
resolveGapDetectionToleranceSeconds(request),
|
||||||
|
sourceSelectionMode
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -723,15 +735,21 @@ public class EsperOperatingPeriodEvaluationService {
|
||||||
: properties.getEsperPoc().getOperatingPeriodEvaluation().getEngineMode();
|
: properties.getEsperPoc().getOperatingPeriodEvaluation().getEngineMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EsperSourceSelectionMode resolveSourceSelectionMode(EsperOperatingPeriodRequest request) {
|
||||||
|
return request.sourceSelectionMode() == null ? EsperSourceSelectionMode.MIXED : request.sourceSelectionMode();
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> notes(
|
private List<String> notes(
|
||||||
EsperOperatingPeriodEngineMode engineMode,
|
EsperOperatingPeriodEngineMode engineMode,
|
||||||
EsperUnknownTreatmentMode unknownTreatmentMode,
|
EsperUnknownTreatmentMode unknownTreatmentMode,
|
||||||
int operatingSplitIdleHours,
|
int operatingSplitIdleHours,
|
||||||
int significantDrivingMinutes,
|
int significantDrivingMinutes,
|
||||||
int gapDetectionToleranceSeconds
|
int gapDetectionToleranceSeconds,
|
||||||
|
EsperSourceSelectionMode sourceSelectionMode
|
||||||
) {
|
) {
|
||||||
return List.of(
|
return List.of(
|
||||||
"This endpoint runs in parallel to the existing working-shift PoC and does not change its semantics.",
|
"This endpoint runs in parallel to the existing working-shift PoC and does not change its semantics.",
|
||||||
|
"Source selection mode is " + sourceSelectionMode + ".",
|
||||||
"BREAK_REST events are ignored for activity evaluation but still prevent synthetic UNKNOWN intervals from being created over covered rest spans.",
|
"BREAK_REST events are ignored for activity evaluation but still prevent synthetic UNKNOWN intervals from being created over covered rest spans.",
|
||||||
"Synthetic UNKNOWN intervals are created only for uncovered gaps between non-rest activities.",
|
"Synthetic UNKNOWN intervals are created only for uncovered gaps between non-rest activities.",
|
||||||
"UNKNOWN treatment mode is " + unknownTreatmentMode + ".",
|
"UNKNOWN treatment mode is " + unknownTreatmentMode + ".",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package at.procon.eventhub.esperpoc.api;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentResultDto;
|
||||||
|
import at.procon.eventhub.esperpoc.service.EsperDtiEnrichmentService;
|
||||||
|
import at.procon.eventhub.esperpoc.service.EsperOperatingPeriodEvaluationService;
|
||||||
|
import at.procon.eventhub.esperpoc.service.EsperPocDriverCardActivityService;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
class EsperPocControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesDtiEnrichmentEndpoint() throws Exception {
|
||||||
|
EsperPocDriverCardActivityService activityService = org.mockito.Mockito.mock(EsperPocDriverCardActivityService.class);
|
||||||
|
EsperOperatingPeriodEvaluationService operatingService = org.mockito.Mockito.mock(EsperOperatingPeriodEvaluationService.class);
|
||||||
|
EsperDtiEnrichmentService enrichmentService = org.mockito.Mockito.mock(EsperDtiEnrichmentService.class);
|
||||||
|
EsperPocController controller = new EsperPocController(activityService, operatingService, enrichmentService);
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
|
|
||||||
|
when(enrichmentService.evaluate(any())).thenReturn(new EsperDtiEnrichmentResultDto(
|
||||||
|
"default",
|
||||||
|
UUID.fromString("00000000-0000-0000-0000-000000000123"),
|
||||||
|
OffsetDateTime.parse("2026-04-01T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-02T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-03-31T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-03T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-03-01T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-03T00:00:00Z"),
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
180,
|
||||||
|
180,
|
||||||
|
720,
|
||||||
|
List.of(),
|
||||||
|
List.of("note")
|
||||||
|
));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/eventhub/esper-poc/tachograph/dti-enrichment")
|
||||||
|
.param("tenantKey", "default")
|
||||||
|
.param("driverId", "00000000-0000-0000-0000-000000000123")
|
||||||
|
.param("occurredFrom", "2026-04-01T00:00:00Z")
|
||||||
|
.param("occurredTo", "2026-04-02T00:00:00Z"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.tenantKey").value("default"))
|
||||||
|
.andExpect(jsonPath("$.pureDtiCount").value(1))
|
||||||
|
.andExpect(jsonPath("$.vehicleUsageIntervalCount").value(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
package at.procon.eventhub.esperpoc.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryPositionDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryVehicleDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DtiBoundaryVicinityEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.DrivingInterruptionDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EnrichedDtiIntervalDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperDtiEnrichmentRequest;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodResultDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSupportEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.OperatingPeriodDto;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.ShiftDrivingEvaluationDto;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class EsperDtiEnrichmentServiceTest {
|
||||||
|
|
||||||
|
private final EsperDtiEnrichmentService service = new EsperDtiEnrichmentService(null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergesVehicleIntervalsAndPrefersIwCycleAsAuthoritativeSource() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
UUID vehicleId = UUID.randomUUID();
|
||||||
|
UUID registrationId = UUID.randomUUID();
|
||||||
|
|
||||||
|
List<EsperDtiEnrichmentService.ResolvedVehicleUsageInterval> merged = service.mergeVehicleUsageIntervals(
|
||||||
|
service.buildVehicleUsageIntervals(List.of(
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "CARD_VEHICLES_USED", "cv1", "2026-04-01T08:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "CARD_VEHICLES_USED", "cv1", "2026-04-01T12:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "IW_CYCLE", "iw1", "2026-04-01T08:05:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "IW_CYCLE", "iw1", "2026-04-01T11:55:00Z", null, null)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(merged).hasSize(1);
|
||||||
|
assertThat(merged.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z"));
|
||||||
|
assertThat(merged.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T12:00:00Z"));
|
||||||
|
assertThat(merged.get(0).authoritativeSource()).isEqualTo("IW_CYCLE");
|
||||||
|
assertThat(merged.get(0).sourceRowIds()).containsExactly("cv1", "iw1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergesCardVehiclesUsedAcrossMidnightBoundaryWhenCardRemainsInVehicle() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
UUID vehicleId = UUID.randomUUID();
|
||||||
|
UUID firstRegistrationId = UUID.randomUUID();
|
||||||
|
UUID secondRegistrationId = UUID.randomUUID();
|
||||||
|
|
||||||
|
List<EsperDtiEnrichmentService.ResolvedVehicleUsageInterval> merged = service.mergeVehicleUsageIntervals(
|
||||||
|
service.buildVehicleUsageIntervals(List.of(
|
||||||
|
supportEvent(driverId, vehicleId, firstRegistrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "CARD_VEHICLES_USED", "cv1", "2026-04-01T08:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, firstRegistrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "CARD_VEHICLES_USED", "cv1", "2026-04-01T23:59:59Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, secondRegistrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "CARD_VEHICLES_USED", "cv2", "2026-04-02T00:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, secondRegistrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "CARD_VEHICLES_USED", "cv2", "2026-04-02T12:00:00Z", null, null)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(merged).hasSize(1);
|
||||||
|
assertThat(merged.get(0).authoritativeSource()).isEqualTo("CARD_VEHICLES_USED");
|
||||||
|
assertThat(merged.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z"));
|
||||||
|
assertThat(merged.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-02T12:00:00Z"));
|
||||||
|
assertThat(merged.get(0).sourceRowIds()).containsExactly("cv1", "cv2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void condensesConsecutiveCardVehiclesUsedSupportEventsToMergedBoundaryEvents() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
UUID vehicleId = UUID.randomUUID();
|
||||||
|
UUID firstRegistrationId = UUID.randomUUID();
|
||||||
|
UUID secondRegistrationId = UUID.randomUUID();
|
||||||
|
|
||||||
|
List<EsperSupportEventDto> condensed = service.condenseSupportEvents(List.of(
|
||||||
|
supportEvent(driverId, vehicleId, firstRegistrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "CARD_VEHICLES_USED", "cv1", "2026-04-01T08:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, firstRegistrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "CARD_VEHICLES_USED", "cv1", "2026-04-01T23:59:59Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, secondRegistrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "CARD_VEHICLES_USED", "cv2", "2026-04-02T00:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, secondRegistrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "CARD_VEHICLES_USED", "cv2", "2026-04-02T12:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, secondRegistrationId, "POSITION", "POSITION_RECORDED", "SNAPSHOT", "CARD_POSITION", "pos1", "2026-04-02T00:05:00Z", "48.2082", "16.3738")
|
||||||
|
));
|
||||||
|
|
||||||
|
assertThat(condensed).hasSize(3);
|
||||||
|
assertThat(condensed)
|
||||||
|
.extracting(EsperSupportEventDto::extractionCode, EsperSupportEventDto::eventType, EsperSupportEventDto::sourceRowId)
|
||||||
|
.containsExactly(
|
||||||
|
org.assertj.core.groups.Tuple.tuple("CARD_VEHICLES_USED", "CARD_INSERTED", "cv1"),
|
||||||
|
org.assertj.core.groups.Tuple.tuple("CARD_POSITION", "POSITION_RECORDED", "pos1"),
|
||||||
|
org.assertj.core.groups.Tuple.tuple("CARD_VEHICLES_USED", "CARD_WITHDRAWN", "cv2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesBoundaryVehicleGeoAndVicinityFromSupportEvents() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
UUID vehicleId = UUID.randomUUID();
|
||||||
|
UUID registrationId = UUID.randomUUID();
|
||||||
|
OffsetDateTime start = OffsetDateTime.parse("2026-04-01T09:00:00Z");
|
||||||
|
OffsetDateTime end = OffsetDateTime.parse("2026-04-01T10:00:00Z");
|
||||||
|
|
||||||
|
List<EsperSupportEventDto> supportEvents = List.of(
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_INSERTED", "INSERT", "IW_CYCLE", "iw1", "2026-04-01T08:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "DRIVER_CARD", "CARD_WITHDRAWN", "WITHDRAW", "IW_CYCLE", "iw1", "2026-04-01T12:00:00Z", null, null),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "POSITION", "POSITION_RECORDED", "SNAPSHOT", "VU_POSITION", "p1", "2026-04-01T09:01:00Z", "48.2082", "16.3738"),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "LOAD_UNLOAD", "LOAD", "SNAPSHOT", "VU_LOAD_UNLOAD", "lu1", "2026-04-01T09:03:00Z", "48.2085", "16.3740"),
|
||||||
|
supportEvent(driverId, vehicleId, registrationId, "PLACE", "WORKING_DAY_PLACE_RECORDED", "END", "VU_PLACE", "pl1", "2026-04-01T09:59:00Z", "48.2090", "16.3750")
|
||||||
|
);
|
||||||
|
List<EsperDtiEnrichmentService.ResolvedVehicleUsageInterval> usageIntervals = service.mergeVehicleUsageIntervals(
|
||||||
|
service.buildVehicleUsageIntervals(supportEvents)
|
||||||
|
);
|
||||||
|
|
||||||
|
EnrichedDtiIntervalDto enriched = service.enrichInterval(
|
||||||
|
new EsperDtiEnrichmentService.PureDtiInterval(
|
||||||
|
"DTI-1",
|
||||||
|
driverId,
|
||||||
|
"BETWEEN_SIGNIFICANT_DRIVING",
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
3600,
|
||||||
|
1,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z"),
|
||||||
|
"d-prev",
|
||||||
|
"d-next"
|
||||||
|
),
|
||||||
|
supportEvents,
|
||||||
|
usageIntervals,
|
||||||
|
new EsperDtiEnrichmentRequest(
|
||||||
|
"default",
|
||||||
|
driverId,
|
||||||
|
OffsetDateTime.parse("2026-04-01T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-02T00:00:00Z"),
|
||||||
|
24,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
EsperSourceSelectionMode.MIXED,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
24 * 30,
|
||||||
|
180,
|
||||||
|
180
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DtiBoundaryVehicleDto beginVehicle = enriched.beginVehicle();
|
||||||
|
assertThat(beginVehicle).isNotNull();
|
||||||
|
assertThat(beginVehicle.vehicleId()).isEqualTo(vehicleId);
|
||||||
|
assertThat(beginVehicle.resolutionSource()).isEqualTo("IW_CYCLE");
|
||||||
|
assertThat(enriched.intervalKind()).isEqualTo("BETWEEN_SIGNIFICANT_DRIVING");
|
||||||
|
|
||||||
|
DtiBoundaryPositionDto beginPosition = enriched.beginPosition();
|
||||||
|
assertThat(beginPosition).isNotNull();
|
||||||
|
assertThat(beginPosition.eventDomain()).isEqualTo("POSITION");
|
||||||
|
assertThat(beginPosition.position().latitude()).isEqualByComparingTo("48.2082");
|
||||||
|
|
||||||
|
DtiBoundaryPositionDto endPosition = enriched.endPosition();
|
||||||
|
assertThat(endPosition).isNotNull();
|
||||||
|
assertThat(endPosition.eventDomain()).isEqualTo("PLACE");
|
||||||
|
|
||||||
|
List<DtiBoundaryVicinityEventDto> beginVicinity = enriched.beginVicinityEvents();
|
||||||
|
assertThat(beginVicinity).extracting(DtiBoundaryVicinityEventDto::eventDomain)
|
||||||
|
.contains("DRIVER_CARD", "POSITION", "LOAD_UNLOAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractsPureDtiFromOperatingPeriodDrivingInterruptions() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
OffsetDateTime requestedFrom = OffsetDateTime.parse("2026-04-01T00:00:00Z");
|
||||||
|
OffsetDateTime requestedTo = OffsetDateTime.parse("2026-04-02T00:00:00Z");
|
||||||
|
|
||||||
|
EsperOperatingPeriodResultDto result = new EsperOperatingPeriodResultDto(
|
||||||
|
"default",
|
||||||
|
driverId,
|
||||||
|
requestedFrom,
|
||||||
|
requestedTo,
|
||||||
|
requestedFrom.minusHours(24),
|
||||||
|
requestedTo.plusHours(24),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
EsperSourceSelectionMode.MIXED,
|
||||||
|
EsperUnknownTreatmentMode.AS_BREAK_REST,
|
||||||
|
EsperOperatingPeriodEngineMode.STREAM_COLLECTOR,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(new OperatingPeriodDto(
|
||||||
|
4,
|
||||||
|
OffsetDateTime.parse("2026-04-01T06:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-01T18:00:00Z"),
|
||||||
|
12 * 3600L,
|
||||||
|
"FLUSH",
|
||||||
|
List.of(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
new ShiftDrivingEvaluationDto(
|
||||||
|
3,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-01T16:00:00Z"),
|
||||||
|
activity(driverId, "2026-04-01T08:00:00Z", "2026-04-01T10:00:00Z", "d1"),
|
||||||
|
activity(driverId, "2026-04-01T14:00:00Z", "2026-04-01T16:00:00Z", "d2"),
|
||||||
|
List.of(new DrivingInterruptionDto(
|
||||||
|
OffsetDateTime.parse("2026-04-01T10:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-01T14:00:00Z"),
|
||||||
|
14400,
|
||||||
|
"d1",
|
||||||
|
"d2"
|
||||||
|
))
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)),
|
||||||
|
List.of("note")
|
||||||
|
);
|
||||||
|
|
||||||
|
List<EsperDtiEnrichmentService.PureDtiInterval> pureDtiIntervals = service.extractPureDtiIntervals(result);
|
||||||
|
|
||||||
|
assertThat(pureDtiIntervals).hasSize(3);
|
||||||
|
assertThat(pureDtiIntervals).extracting(EsperDtiEnrichmentService.PureDtiInterval::intervalKind)
|
||||||
|
.containsExactly("BEFORE_FIRST_SIGNIFICANT_DRIVING", "BETWEEN_SIGNIFICANT_DRIVING", "AFTER_LAST_SIGNIFICANT_DRIVING");
|
||||||
|
assertThat(pureDtiIntervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T06:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(0).previousDrivingSourceRowId()).isNull();
|
||||||
|
assertThat(pureDtiIntervals.get(0).nextDrivingSourceRowId()).isEqualTo("d1");
|
||||||
|
|
||||||
|
assertThat(pureDtiIntervals.get(1).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T10:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(1).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T14:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(1).operatingPeriodNo()).isEqualTo(4);
|
||||||
|
assertThat(pureDtiIntervals.get(1).previousDrivingSourceRowId()).isEqualTo("d1");
|
||||||
|
assertThat(pureDtiIntervals.get(1).nextDrivingSourceRowId()).isEqualTo("d2");
|
||||||
|
|
||||||
|
assertThat(pureDtiIntervals.get(2).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T16:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(2).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T18:00:00Z"));
|
||||||
|
assertThat(pureDtiIntervals.get(2).previousDrivingSourceRowId()).isEqualTo("d2");
|
||||||
|
assertThat(pureDtiIntervals.get(2).nextDrivingSourceRowId()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private at.procon.eventhub.esperpoc.dto.ActivityIntervalDto activity(
|
||||||
|
UUID driverId,
|
||||||
|
String from,
|
||||||
|
String to,
|
||||||
|
String sourceRowId
|
||||||
|
) {
|
||||||
|
return at.procon.eventhub.esperpoc.dto.ActivityIntervalDto.raw(
|
||||||
|
driverId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"KNOWN",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
OffsetDateTime.parse(from),
|
||||||
|
OffsetDateTime.parse(to),
|
||||||
|
sourceRowId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EsperSupportEventDto supportEvent(
|
||||||
|
UUID driverId,
|
||||||
|
UUID vehicleId,
|
||||||
|
UUID registrationId,
|
||||||
|
String eventDomain,
|
||||||
|
String eventType,
|
||||||
|
String lifecycle,
|
||||||
|
String extractionCode,
|
||||||
|
String sourceRowId,
|
||||||
|
String occurredAt,
|
||||||
|
String latitude,
|
||||||
|
String longitude
|
||||||
|
) {
|
||||||
|
return new EsperSupportEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
OffsetDateTime.parse(occurredAt),
|
||||||
|
sourceRowId,
|
||||||
|
extractionCode + ":" + sourceRowId + ":" + eventType,
|
||||||
|
extractionCode.startsWith("VU_") || "IW_CYCLE".equals(extractionCode) ? "VEHICLE_UNIT" : "DRIVER_CARD",
|
||||||
|
extractionCode,
|
||||||
|
driverId,
|
||||||
|
UUID.randomUUID(),
|
||||||
|
vehicleId,
|
||||||
|
registrationId,
|
||||||
|
eventDomain,
|
||||||
|
eventType,
|
||||||
|
lifecycle,
|
||||||
|
"DRIVER",
|
||||||
|
latitude == null ? null : new BigDecimal(latitude),
|
||||||
|
longitude == null ? null : new BigDecimal(longitude),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"LOAD".equals(eventType) ? "LOAD" : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import at.procon.eventhub.esperpoc.dto.ActivityIntervalDto;
|
import at.procon.eventhub.esperpoc.dto.ActivityIntervalDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
|
||||||
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
|
||||||
|
import at.procon.eventhub.esperpoc.dto.RawActivityEventDto;
|
||||||
|
import at.procon.eventhub.esperpoc.persistence.EsperPocActivityRepository;
|
||||||
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
|
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
|
||||||
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
|
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
@ -12,6 +16,10 @@ import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class EsperOperatingPeriodEvaluationServiceTest {
|
class EsperOperatingPeriodEvaluationServiceTest {
|
||||||
|
|
||||||
|
|
@ -132,6 +140,46 @@ class EsperOperatingPeriodEvaluationServiceTest {
|
||||||
.isEqualTo(collectorEvaluation.closedPeriods());
|
.isEqualTo(collectorEvaluation.closedPeriods());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void driverCardOnlyModeIgnoresVehicleUnitGapFill() {
|
||||||
|
UUID driverId = UUID.randomUUID();
|
||||||
|
EsperPocActivityRepository repository = mock(EsperPocActivityRepository.class);
|
||||||
|
EsperOperatingPeriodEvaluationService evaluationService = new EsperOperatingPeriodEvaluationService(
|
||||||
|
repository,
|
||||||
|
new EsperDriverActivityEngine(),
|
||||||
|
operatingPeriodEngine
|
||||||
|
);
|
||||||
|
when(repository.findDriverActivityEvents(eq("default"), eq(driverId), any(), any())).thenReturn(List.of(
|
||||||
|
raw(driverId, "DRIVER_CARD", "DRIVE", "START", "2026-04-01T08:00:00Z", "card-1"),
|
||||||
|
raw(driverId, "DRIVER_CARD", "DRIVE", "END", "2026-04-01T09:00:00Z", "card-1"),
|
||||||
|
raw(driverId, "VEHICLE_UNIT", "DRIVE", "START", "2026-04-01T09:00:00Z", "vu-1"),
|
||||||
|
raw(driverId, "VEHICLE_UNIT", "DRIVE", "END", "2026-04-01T10:00:00Z", "vu-1")
|
||||||
|
));
|
||||||
|
|
||||||
|
var result = evaluationService.evaluate(new EsperOperatingPeriodRequest(
|
||||||
|
"default",
|
||||||
|
driverId,
|
||||||
|
OffsetDateTime.parse("2026-04-01T00:00:00Z"),
|
||||||
|
OffsetDateTime.parse("2026-04-02T00:00:00Z"),
|
||||||
|
24,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
EsperSourceSelectionMode.DRIVER_CARD_ONLY,
|
||||||
|
EsperUnknownTreatmentMode.AS_BREAK_REST,
|
||||||
|
EsperOperatingPeriodEngineMode.STREAM_COLLECTOR
|
||||||
|
));
|
||||||
|
|
||||||
|
assertThat(result.rawEventCount()).isEqualTo(2);
|
||||||
|
assertThat(result.driverCardRawEventCount()).isEqualTo(2);
|
||||||
|
assertThat(result.vehicleUnitRawEventCount()).isZero();
|
||||||
|
assertThat(result.driverCardIntervalCount()).isEqualTo(1);
|
||||||
|
assertThat(result.vehicleUnitIntervalCount()).isZero();
|
||||||
|
assertThat(result.resolvedKnownIntervalCount()).isEqualTo(1);
|
||||||
|
assertThat(result.rawEvents()).extracting(RawActivityEventDto::sourceKind).containsOnly("DRIVER_CARD");
|
||||||
|
}
|
||||||
|
|
||||||
private ActivityIntervalDto activity(
|
private ActivityIntervalDto activity(
|
||||||
UUID driverId,
|
UUID driverId,
|
||||||
String activity,
|
String activity,
|
||||||
|
|
@ -184,4 +232,31 @@ class EsperOperatingPeriodEvaluationServiceTest {
|
||||||
0L
|
0L
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RawActivityEventDto raw(
|
||||||
|
UUID driverId,
|
||||||
|
String sourceKind,
|
||||||
|
String eventType,
|
||||||
|
String lifecycle,
|
||||||
|
String occurredAt,
|
||||||
|
String sourceRowId
|
||||||
|
) {
|
||||||
|
return new RawActivityEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
OffsetDateTime.parse(occurredAt),
|
||||||
|
sourceRowId,
|
||||||
|
sourceKind + ":" + sourceRowId + ":" + lifecycle,
|
||||||
|
sourceKind,
|
||||||
|
"DRIVER_CARD".equals(sourceKind) ? "CARD_ACTIVITY" : "VU_ACTIVITY",
|
||||||
|
driverId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
eventType,
|
||||||
|
lifecycle,
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"KNOWN",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue