我有一个 java spring boot 后端应用程序,它将从 mongo 获取无人认领的访问,然后使用 optaplanner 找到覆盖提供商/临床医生的这些访问的最佳路线。在我的约束提供者类中也有一些类似于 VRP 的约束。问题是我在变量更改侦听器中收到 NPE,因为出发时间为空,即使 previousVisit 不为空也是如此。
Visits previousVisit = visit.getPreviousVisit();
LocalDateTime departureTime =
previousVisit == null ? visit.getAssignedClinician().getDepartureTime() : previousVisit.getDepartureTime();
Visits nextVisit = visit;
LocalDateTime arrivalTime = previousVisit == null ?
departureTime.plusMinutes(getDrivingTime(visit.getAssignedClinician().getLocation().getCoordinates(), visit.getMember().getMemberLocation().getCoordinates())) :
departureTime.plusMinutes(getDrivingTime(previousVisit.getMember().getMemberLocation().getCoordinates(), nextVisit.getMember().getMemberLocation().getCoordinates()));
**注意:由于我想安排访问而不是对提供商的日程进行实时更改,我的用例是否需要变量更改侦听器?如果没有,请让我知道我在哪里犯了错误或者设计是否需要更改。谢谢并非常感谢。 **
下面是我的后端应用程序的基本用例。
用例:
临床医生/提供者一天内有空,会员也有空,提供者时段可以分为 1 小时时段。提供者将从家里开始进行访问。我们需要为提供商找到最佳的访问者。下面是示例。这是带有附加约束的车辆路径问题时间窗口。
现在我们需要检查的不是距离,而是驾驶时间和 1 小时的访问时段,所以假设提供商时段为上午 8 点至 11 点,平均访问时间为 55 分钟,会员的访问窗口为上午 8 点至 10 点,那么提供商应到达会员位置在上午 8 点至 10 点之间,并在该时间范围内完成 55 分钟的访问,所以基本上这是车辆路线问题,涉及提供商和会员的时间窗口,并考虑驾驶时间和访问时间。因此,我们将使用 Mapbox api 来计算行驶时间,并假设访问时间为 55 分钟,而不是半正矢距离。
以下示例路线供提供商澄清更多信息:
提供商时段:上午 8 点至 11 点
访问 1(时间窗口上午 8 点至上午 10 点):开车时间 20 分钟,早上 7:40 离开家,早上 8 点到达并访问 55 分钟,即直到上午 8:55(在访问时间窗口内)分配的提供商时段:上午 8 点至上午 9 点
访问 1 访问 2(时间窗口 9-11am):车程 20 分钟,于上午 9:15 到达并访问 55 分钟,即直到上午 10:05(在访问时间窗口内)分配的提供商时段:上午 9-10 点
访问 2 访问 3(时间窗口 10-11am):车程 20 分钟,上午 10:25 到达并访问 55 分钟,即到上午 11:20(在访问时间窗口内),因此不应分配此访问至提供商 未分配提供商插槽
规划单位1:
@PlanningEntity
@Data
public class Visits {
private String _id;
private Double matrixVisitId;
private Long clientId;
private VisitMsa msa;
private Date[] slotPreferences;
private String[] products;
private VisitsMember member;
private String slotType;
private boolean pinned;
private TimeWindow timeWindow;
private ClinicianSlot assignedSlot;
private Providers assignedClinician;
private Visits previousVisit;
private Visits nextVisit;
private LocalDateTime arrivalTime;
@ValueRangeProvider(id = "timeBlockRangeForVisit")
public List<ClinicianSlot> generatePossibleTimeBlocks() {
if (this.timeWindow == null) {
return Collections.emptyList(); // Return an empty list for home visit
}
List<ClinicianSlot> possibleTimeBlocks = new ArrayList<>();
LocalDateTime start = timeWindow.getStart();
while (!start.plusHours(1).isAfter(timeWindow.getEnd())) {
possibleTimeBlocks.add(new ClinicianSlot(start, start.plusHours(1)));
start = start.plusHours(1);
}
return possibleTimeBlocks;
}
@PlanningPin
public boolean isPinned() {
return pinned;
}
@PreviousElementShadowVariable(sourceVariableName = "visits")
public Visits getPreviousVisit() {
return previousVisit;
}
public void setPreviousVisit(Visits previousVisit) {
this.previousVisit = previousVisit;
}
@NextElementShadowVariable(sourceVariableName = "visits")
public Visits getNextVisit() {
return nextVisit;
}
public void setNextVisit(Visits nextVisit) {
this.nextVisit = nextVisit;
}
@InverseRelationShadowVariable(sourceVariableName = "visits")
public Providers getAssignedClinician() {
return assignedClinician;
}
public void setVehicle(Providers assignedClinician) {
this.assignedClinician = assignedClinician;
}
@ShadowVariable(variableListenerClass = VisitChainVariableListener.class, sourceVariableName = "previousVisit")
public LocalDateTime getArrivalTime() {
return arrivalTime;
}
public void setArrivalTime(LocalDateTime arrivalTime) {
this.arrivalTime = arrivalTime;
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public LocalDateTime getDepartureTime() {
if (arrivalTime == null) {
return null;
}
return getStartServiceTime().plusMinutes(55);
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public LocalDateTime getStartServiceTime() {
if (arrivalTime == null) {
return null;
}
return arrivalTime.isBefore(minStartTime) ? minStartTime : arrivalTime;
}
}
**规划实体2:**
@Data
@PlanningEntity
@AllArgsConstructor
@NoArgsConstructor
public class Providers {
private Integer staffResourceId;
private String providerId;
private ProviderMsa[] msa;
private Client[] clients;
private String[] products;
private List<ClinicianSlot> availableSlots;
@PlanningListVariable(valueRangeProviderRefs = "visitRange")
private List<Visits> visits = new ArrayList<>();
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private Location location;
private LocalDateTime departureTime;
}
解决方案类:
@PlanningSolution
@Data
public class ProviderRouteSolution {
private List<Visits> visitList;
@ProblemFactCollectionProperty
@ValueRangeProvider(id = "visitRange")
public List<Visits> getVisitList() {
return visitList;
}
private List<Providers> clinicianList;
@PlanningScore
private HardSoftScore score;
@ValueRangeProvider(id = "clinicianRange")
@PlanningEntityCollectionProperty
public List<Providers> getClinicianList() {
return clinicianList;
}
}
变量变化监听器类:
public class VisitChainVariableListener implements VariableListener<ProviderRouteSolution, Visits> {
@Override
public void afterVariableChanged(ScoreDirector<ProviderRouteSolution> scoreDirector, Visits visit) {
if (visit.getAssignedClinician() == null) {
if (visit.getArrivalTime() != null) {
scoreDirector.beforeVariableChanged(visit, "arrivalTime");
visit.setArrivalTime(null);
scoreDirector.afterVariableChanged(visit, "arrivalTime");
}
return;
}
Visits previousVisit = visit.getPreviousVisit();
LocalDateTime departureTime =
previousVisit == null ? visit.getAssignedClinician().getDepartureTime() : previousVisit.getDepartureTime();
Visits nextVisit = visit;
LocalDateTime arrivalTime = previousVisit == null ?
departureTime.plusMinutes(getDrivingTime(visit.getAssignedClinician().getLocation().getCoordinates(), visit.getMember().getMemberLocation().getCoordinates())) :
departureTime.plusMinutes(getDrivingTime(previousVisit.getMember().getMemberLocation().getCoordinates(), nextVisit.getMember().getMemberLocation().getCoordinates()));
while (nextVisit != null && !Objects.equals(nextVisit.getArrivalTime(), arrivalTime)) {
scoreDirector.beforeVariableChanged(nextVisit, "arrivalTime");
nextVisit.setArrivalTime(arrivalTime);
scoreDirector.afterVariableChanged(nextVisit, "arrivalTime");
departureTime = nextVisit.getDepartureTime();
Double[] departureCoordinates = nextVisit.getMember().getMemberLocation().getCoordinates();
nextVisit = nextVisit.getNextVisit();
arrivalTime = departureTime.plusMinutes(getDrivingTime(departureCoordinates, visit.getMember().getMemberLocation().getCoordinates()));
}
}
}
提供商路线服务:
public List<String> getProviderRouteForTheDay(int staffResourceId, double radiusInMiles, String visitDateString) throws ParseException {
Providers provider = providersRepository.findByStaffResourceId(staffResourceId);
Double longitude = provider.getLocation().getCoordinates()[0];
Double latitude = provider.getLocation().getCoordinates()[1];
double radiusInMeters = radiusInMiles * 1609.34;
Location location = new Location(latitude, longitude);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date visitDate = dateFormat.parse(visitDateString);
Calendar startCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
startCal.setTime(visitDate);
startCal.set(Calendar.HOUR_OF_DAY, 0);
startCal.set(Calendar.MINUTE, 0);
startCal.set(Calendar.SECOND, 0);
startCal.set(Calendar.MILLISECOND, 0);
Date startOfDay = startCal.getTime();
Calendar endCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
endCal.setTime(visitDate);
endCal.set(Calendar.HOUR_OF_DAY, 23);
endCal.set(Calendar.MINUTE, 59);
endCal.set(Calendar.SECOND, 59);
endCal.set(Calendar.MILLISECOND, 999);
Date endOfDay = endCal.getTime();
System.out.println("Start of Day: " + startOfDay);
System.out.println("End of Day: " + endOfDay);
List<ProviderSlots> providerSlots = providerSlotsRepository.findSlotsByProviderAndDateRangeAndStatus(provider.getStaffResourceId(), startOfDay, endOfDay, "available");
List<ClinicianSlot> clinicianSlots = new ArrayList<>();
for (ProviderSlots slot : providerSlots) {
clinicianSlots.addAll(generate1HourSlots(convertToLocalDateTime(slot.getSlotTime()), convertToLocalDateTime(slot.getSlotEndTime())));
}
provider.setAvailableSlots(clinicianSlots);
provider.setDepartureTime(LocalDateTime.of(
LocalDateTime.now(ZoneOffset.UTC).getYear(),
LocalDateTime.now(ZoneOffset.UTC).getMonth(),
LocalDateTime.now(ZoneOffset.UTC).getDayOfMonth(),
12, 0
));
List<Visits> visitsList = visitsRepository.findUnclaimedVisitsNear(location.getLongitude(), location.getLatitude(), radiusInMeters, startOfDay, endOfDay);
assignVisitTimeWindows(visitsList);
ProviderRouteSolution unsolvedSolution = new ProviderRouteSolution();
unsolvedSolution.setVisitList(visitsList);
unsolvedSolution.setClinicianList(Collections.singletonList(provider));
initializeDrivingTimeMatrix(provider, visitsList);
try {
ProviderRouteSolution solvedSolution = solver.solve(unsolvedSolution);
} catch(Exception ex) {
ex.printStackTrace();
}
return null;
}
我想找到提供商的最佳访问路线,在变量链侦听器类中获得 NPE。我不确定我的实施是否正确,因为我不是在寻找 optaweb-VRP,我正在寻找链接访问以找到提供商的优化路径。
在最新版本之一中,实现影子变量侦听器的机制已得到极大简化。您可以在官方网站上查看有关该内容的博客文章。
对于示例中的使用,快速入门已进行了相应调整。我认为几乎您需要的所有内容都将包含在找到的车辆路线示例中。这包括通过 @PlanningListVariable 注释进行链式访问。在快速入门中,此处使用了该注释。