Commit 1a09ae86 by 张晓

Merge branch 'develop' of https://gitlab.dituhui.com/bsh/project/project into develop

2 parents 5f0bc92b 9037490e
package com.dituhui.pea.dispatch.constraint;
/**
* 约束枚举
*
* @author zhangguoping
*
*/
public enum ConstraintNameEnum {
// 硬约束
/**
* 技术员技能跟订单相匹配
*/
skillMatch,
/**
* 技术员到达时间跟时间窗吻合
*/
customerTimeWindowsMatch,
// 软约束
/**
* 订单数量均衡
*/
technicianBalanceSoft,
/**
* 总路程最小
*/
totalDistance,
/**
* 技术员中心点偏好
*/
preferredTotalDistance
}
......@@ -12,38 +12,53 @@ public class DispatchConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] { greaterThanZero(factory), customerTimeWindowsMatch1(factory),
customerTimeWindowsMatch2(factory), skillMatch(factory), technicianBalance(factory),
technicianBalance2(factory), technicianBalanceSoft(factory), totalDistance(factory),
preferredTotalDistance(factory) };
return new Constraint[] {
// 硬约束
// 运行时长提升效果
// 5s/60s: 技能权重4-技能匹配问题0个,时间窗问题14个
// 10s/300s: 技能权重4-技能匹配问题0个,时间窗问题9个
customerTimeWindowsMatch(factory),
skillMatch(factory),
// 软约束
technicianBalanceSoft(factory),
totalDistance(factory),
preferredTotalDistance(factory)
};
}
// ************************************************************************
// Hard constraints
// ************************************************************************
// 1,每个技术员至少分配一个greaterThanZero
// 2,技术员到达时间跟时间窗吻合customerTimeWindowsMatch
// 3,技术员时间窗吻合technicianTimeWindowsMatch
// 4,技术员技能跟订单相匹配skillMatch
// 5,订单均分technicianBalance
// 1,技术员到达时间跟时间窗吻合customerTimeWindowsMatch
// 2,技术员时间窗吻合technicianTimeWindowsMatch
// 3,技术员技能跟订单相匹配skillMatch
public Constraint greaterThanZero(ConstraintFactory factory) {
return factory.forEach(Technician.class).filter(technician -> technician.getCustomerList().size() == 0)
.penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("每个技术员至少分配一个单子");
}
protected Constraint customerTimeWindowsMatch1(ConstraintFactory factory) {
protected Constraint customerTimeWindowsMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class).filter(
customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime())
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合1");
.penalizeLong(HardSoftLongScore.ONE_HARD,
// 迟到每2小时扣一分
customer -> (long) Math.ceil((customer.getArrivalTime() - customer.getEndTime()) / 120F))
.asConstraint(ConstraintNameEnum.customerTimeWindowsMatch.name());
}
protected Constraint customerTimeWindowsMatch2(ConstraintFactory factory) {
// 迟到2小时惩罚
return factory.forEach(Customer.class).filter(
customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime() + 120)
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合2");
}
// protected Constraint customerTimeWindowsMatch1(ConstraintFactory factory) {
// return factory.forEach(Customer.class).filter(
// customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime())
// .penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合1");
// }
//
// protected Constraint customerTimeWindowsMatch2(ConstraintFactory factory) {
// // 迟到2小时惩罚
// return factory.forEach(Customer.class).filter(
// customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime() + 120)
// .penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合2");
// }
protected Constraint skillMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class)
......@@ -54,38 +69,21 @@ public class DispatchConstraintProvider implements ConstraintProvider {
// 3-技能匹配问题1个,时间窗问题8个
// 4-技能匹配问题0个,时间窗问题14个
customer -> 4)
.asConstraint("技术员技能跟订单相匹配skillMatch");
}
protected Constraint technicianBalance(ConstraintFactory factory) {
// 会导致剩余单子集中?
return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() > 1140)
.penalizeLong(HardSoftLongScore.ONE_HARD,
technician -> (long) Math.ceil(technician.getOffWorkTime() / 1440f))
.asConstraint("订单均分");
}
protected Constraint technicianBalance2(ConstraintFactory factory) {
// 单量不能过少 FIXME
return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() <= 960)
.penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("订单均分2");
.asConstraint(ConstraintNameEnum.skillMatch.name());
}
// protected Constraint technicianBalance(ConstraintFactory factory) {
// return factory.forEachUniquePair(Technician.class, Joiners.filtering(
// // 一个人的工作时长超过另外一个人的2倍 FIXME 无效
// (t1, t2) -> (t1.getWorkTime() > t2.getWorkTime() * 2) || (t2.getWorkTime() > t1.getWorkTime() * 2)))
// .penalizeLong(HardSoftLongScore.ONE_HARD, (c1, c2) -> 1).asConstraint("订单均分");
// // 会导致剩余单子集中?
// return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() > 1140)
// .penalizeLong(HardSoftLongScore.ONE_HARD,
// technician -> (long) Math.ceil(technician.getOffWorkTime() / 1440f))
// .asConstraint("订单均分");
// }
// protected Constraint technicianBalance(ConstraintFactory factory) {
// return factory.forEachUniquePair(Technician.class, Joiners.filtering(
// // 一个人的数量是另外一个人的3倍 FIXME 无效
// (t1, t2) -> {
// int size1 = t1.getCustomerSize();
// int size2 = t2.getCustomerSize();
// return (size1 > size2 * 3) || (size2 > size1 * 3);
// })).penalizeLong(HardSoftLongScore.ONE_HARD, (c1, c2) -> 1).asConstraint("订单均分");
//
// protected Constraint technicianBalance2(ConstraintFactory factory) {
// // 单量不能过少 FIXME
// return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() <= 960)
// .penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("订单均分2");
// }
// ************************************************************************
......@@ -97,19 +95,21 @@ public class DispatchConstraintProvider implements ConstraintProvider {
protected Constraint totalDistance(ConstraintFactory factory) {
return factory.forEach(Technician.class)
.penalizeLong(HardSoftLongScore.ONE_SOFT, Technician::getTotalDistanceMeters).asConstraint("总路程最小");
.penalizeLong(HardSoftLongScore.ONE_SOFT, Technician::getTotalDistanceMeters)
.asConstraint(ConstraintNameEnum.totalDistance.name());
}
protected Constraint preferredTotalDistance(ConstraintFactory factory) {
return factory.forEach(Technician.class)
.penalizeLong(HardSoftLongScore.ONE_SOFT, Technician::getPreferredTotalDistanceMeters)
.asConstraint("技术员中心点偏好");
.asConstraint(ConstraintNameEnum.preferredTotalDistance.name());
}
protected Constraint technicianBalanceSoft(ConstraintFactory factory) {
return factory.forEachUniquePair(Technician.class).penalizeLong(HardSoftLongScore.ONE_SOFT,
// 权重需要调节,差距一个相当于多一公里 FIXME 这里应该是时长均衡,不是订单量均衡
(a, b) -> Math.abs(a.getCustomerSize() - b.getCustomerSize()) * 1000).asConstraint("订单均分soft");
(a, b) -> Math.abs(a.getCustomerSize() - b.getCustomerSize()) * 1000)
.asConstraint(ConstraintNameEnum.technicianBalanceSoft.name());
}
}
package com.dituhui.pea.dispatch.eventListener;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
public class DispatchSolverEventListener implements org.optaplanner.core.api.solver.event.SolverEventListener<DispatchSolution> {
@Override
public void bestSolutionChanged(BestSolutionChangedEvent<DispatchSolution> event) {
// TODO Auto-generated method stub
}
}
......@@ -13,6 +13,11 @@ import lombok.Setter;
public class Location {
private final long id;
// 坐标点
private float x;
private float y;
private String code;
// 类型 engineer order
private String type;
......@@ -27,6 +32,12 @@ public class Location {
public Location(long id) {
this.id = id;
}
public Location(long id, float x, float y) {
this.id = id;
this.x = x;
this.y = y;
}
public Location(long id, String code, String type, double longitude, double latitude) {
this.id = id;
......
......@@ -19,194 +19,180 @@ import lombok.Data;
@PlanningEntity
public class Technician {
@PlanningId
private long id;
private String code;
@JsonIgnore
private Depot depot;
// 上班时间窗 分钟480-1080 8-18点
private int startTime;
private int endTime;
// 技能
private Set<String> skills;
// 每日最大单量
private int maxCount;
// 每日最大工作时长
private int maxMinute;
// 单位是米,这里要注意
private int maxDistanceMeter;
// 偏好坐标
// private Location preferredlocation;
// teck code : customer code , distance
@JsonIgnore
private Map<String, Long> preferredlocationDistanceMap = new HashMap<String, Long>();
@PlanningListVariable
private List<Customer> customerList = new ArrayList<>();
public Technician() {
}
public Technician(long id, String code, Depot depot, int startTime, int endTime, Set<String> skills,
Map<String, Long> preferredlocationDistanceMap) {
this.id = id;
this.code = code;
this.depot = depot;
this.startTime = startTime;
this.endTime = endTime;
this.skills = skills;
this.preferredlocationDistanceMap = preferredlocationDistanceMap;
}
public Technician(long id, String code, int maxCount, int maxMinute, int maxDistanceMeter,
Depot depot, int startTime, int endTime, Set<String> skills,
Map<String, Long> preferredlocationDistanceMap) {
this.id = id;
this.code = code;
this.depot = depot;
this.startTime = startTime;
this.endTime = endTime;
this.skills = skills;
this.maxCount = maxCount;
this.maxMinute = maxMinute;
this.maxDistanceMeter = maxDistanceMeter;
this.preferredlocationDistanceMap = preferredlocationDistanceMap;
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* @return route of the vehicle
*/
@JsonIgnore
public List<Location> getRoute() {
if (customerList.isEmpty()) {
return Collections.emptyList();
}
List<Location> route = new ArrayList<Location>();
route.add(depot.getLocation());
for (Customer customer : customerList) {
route.add(customer.getLocation());
}
return route;
}
/**
* 总路线距离
*
* @return
*/
public long getTotalDistanceMeters() {
if (customerList.isEmpty()) {
return 0;
}
long totalDistance = 0;
Location previousLocation = depot.getLocation();
for (Customer customer : customerList) {
totalDistance += previousLocation.getDistanceTo(customer.getLocation());
previousLocation = customer.getLocation();
}
totalDistance += previousLocation.getDistanceTo(depot.getLocation());
return totalDistance;
}
/**
* 获取偏好总距离 所有customer与该技术员的PreferredLocation距离之和
*
* @return
*/
public long getPreferredTotalDistanceMeters() {
if (customerList.isEmpty()) {
return 0;
}
long totalDistance = 0;
for (Customer customer : customerList) {
totalDistance += preferredlocationDistanceMap.get(customer.getCode());
}
return totalDistance;
}
public int getCustomerSize() {
return customerList.size();
}
/**
* 获取总上班时间,第一个订单到最后一个订单时间跨度
*
* @return
*/
public int getWorkTime() {
int size = customerList.size();
if (0 == size) {
return 0;
} else {
return customerList.get(size - 1).getArrivalTime() + customerList.get(size - 1).getServiceDuration()
- customerList.get(0).getArrivalTime();
}
}
/**
* 获取下班时间,最后一个订单完成时间
*
* @return
*/
public int getOffWorkTime() {
int size = customerList.size();
if (0 == size) {
return 0;
} else {
Customer lastCustomer = customerList.get(size - 1);
return lastCustomer.getDepartureTime();
}
}
@Override
public int hashCode() {
return Long.valueOf(this.id).hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof Technician))
return false;
if (obj == this)
return true;
return this.id == ((Technician) obj).getId();
}
@Override
public String toString() {
return "Technician{" +
"id=" + id +
", code='" + code + '\'' +
", depot=" + depot +
", startTime=" + startTime +
", endTime=" + endTime +
", skills=" + skills +
", maxCount=" + maxCount +
", maxMinute=" + maxMinute +
", maxDistanceMeter=" + maxDistanceMeter +
'}';
}
@PlanningId
private long id;
private String code;
@JsonIgnore
private Depot depot;
// 上班时间窗 分钟480-1080 8-18点
private int startTime;
private int endTime;
// 技能
private Set<String> skills;
// 偏好坐标
private Location preferredlocation;
// technician code : customer code , distance
@JsonIgnore
private Map<String, Long> preferredlocationDistanceMap = new HashMap<String, Long>();
// 每日最大单量
private int maxCount;
// 每日最大工作时长
private int maxMinute;
// 单位是米,这里要注意
private int maxDistanceMeter;
@PlanningListVariable
private List<Customer> customerList = new ArrayList<>();
public Technician() {
}
public Technician(long id, String code, Depot depot, int startTime, int endTime, Set<String> skills,
Map<String, Long> preferredlocationDistanceMap, Location preferredlocation) {
this.id = id;
this.code = code;
this.depot = depot;
this.startTime = startTime;
this.endTime = endTime;
this.skills = skills;
this.preferredlocationDistanceMap = preferredlocationDistanceMap;
this.preferredlocation = preferredlocation;
}
public Technician(long id, String code, int maxCount, int maxMinute, int maxDistanceMeter, Depot depot,
int startTime, int endTime, Set<String> skills, Map<String, Long> preferredlocationDistanceMap) {
this.id = id;
this.code = code;
this.depot = depot;
this.startTime = startTime;
this.endTime = endTime;
this.skills = skills;
this.maxCount = maxCount;
this.maxMinute = maxMinute;
this.maxDistanceMeter = maxDistanceMeter;
this.preferredlocationDistanceMap = preferredlocationDistanceMap;
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* @return route of the vehicle
*/
@JsonIgnore
public List<Location> getRoute() {
if (customerList.isEmpty()) {
return Collections.emptyList();
}
List<Location> route = new ArrayList<Location>();
route.add(depot.getLocation());
for (Customer customer : customerList) {
route.add(customer.getLocation());
}
return route;
}
/**
* 总路线距离
*
* @return
*/
public long getTotalDistanceMeters() {
if (customerList.isEmpty()) {
return 0;
}
long totalDistance = 0;
Location previousLocation = depot.getLocation();
for (Customer customer : customerList) {
totalDistance += previousLocation.getDistanceTo(customer.getLocation());
previousLocation = customer.getLocation();
}
totalDistance += previousLocation.getDistanceTo(depot.getLocation());
return totalDistance;
}
/**
* 获取偏好总距离 所有customer与该技术员的PreferredLocation距离之和
*
* @return
*/
public long getPreferredTotalDistanceMeters() {
if (customerList.isEmpty()) {
return 0;
}
long totalDistance = 0;
for (Customer customer : customerList) {
totalDistance += preferredlocationDistanceMap.get(customer.getCode());
}
return totalDistance;
}
public int getCustomerSize() {
return customerList.size();
}
/**
* 获取总上班时间,第一个订单到最后一个订单时间跨度
*
* @return
*/
public int getWorkTime() {
int size = customerList.size();
if (0 == size) {
return 0;
} else {
return customerList.get(size - 1).getArrivalTime() + customerList.get(size - 1).getServiceDuration()
- customerList.get(0).getArrivalTime();
}
}
/**
* 获取下班时间,最后一个订单完成时间
*
* @return
*/
public int getOffWorkTime() {
int size = customerList.size();
if (0 == size) {
return 0;
} else {
Customer lastCustomer = customerList.get(size - 1);
return lastCustomer.getDepartureTime();
}
}
@Override
public int hashCode() {
return Long.valueOf(this.id).hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof Technician))
return false;
if (obj == this)
return true;
return this.id == ((Technician) obj).getId();
}
@Override
public String toString() {
return "Technician{" + "id=" + id + ", code='" + code + '\'' + ", depot=" + depot + ", startTime=" + startTime
+ ", endTime=" + endTime + ", skills=" + skills + ", maxCount=" + maxCount + ", maxMinute=" + maxMinute
+ ", maxDistanceMeter=" + maxDistanceMeter + '}';
}
}
......@@ -16,44 +16,21 @@
package com.dituhui.pea.dispatch.service.impl;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.optaplanner.core.api.score.ScoreExplanation;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.core.api.solver.SolutionManager;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.api.solver.event.BestSolutionChangedEvent;
import org.optaplanner.core.api.solver.event.SolverEventListener;
import org.optaplanner.core.config.solver.SolverConfig;
import org.optaplanner.core.config.solver.termination.TerminationConfig;
import org.optaplanner.persistence.jackson.impl.domain.solution.JacksonSolutionFileIO;
import org.optaplanner.core.impl.solver.DefaultSolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import com.dituhui.pea.common.Result;
import com.dituhui.pea.dispatch.constraint.DispatchConstraintProvider;
import com.dituhui.pea.dispatch.pojo.Customer;
import com.dituhui.pea.dispatch.pojo.Depot;
import com.dituhui.pea.dispatch.eventListener.DispatchSolverEventListener;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.pojo.Location;
import com.dituhui.pea.dispatch.pojo.Technician;
import com.dituhui.pea.dispatch.service.DispatchService;
import com.dituhui.pea.dispatch.utils.DataUtils;
import com.dituhui.pea.dispatch.utils.DispatchSolutionUtils;
/**
* @author gpzhang
......@@ -63,366 +40,23 @@ public class DispatchServiceImpl implements DispatchService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Result<?> manualDispatch(long unimprovedSecondsSpentLimit, long secondsSpentLimit) throws UncheckedIOException, IOException {
logger.info("{}", "invoke manualDispatch");
// 创建解决方案对象
Map<Integer, String> customerIndexMap = loadCustomerIndex();
Map<Integer, String> technicianIndexMap = loadTechnicianIndex();
Map<String, Set<String>> technicianCodeSkillsMap = loadTechnicianCodeSkillsMap();
Map<String, String> customerCodeSkillMap = loadCustomerCodeSkillMap();
Map<String, Map<String, Long>> preferredlocationDistanceMap = loadPreferredlocationDistanceMap();
Map<String, Integer> customerCodeServiceTimeMap = loadCustomerCodeServiceTimeMap();
DispatchSolution problem = createVehicleRoutingSolution(customerIndexMap, technicianIndexMap,
technicianCodeSkillsMap, customerCodeSkillMap, preferredlocationDistanceMap,
customerCodeServiceTimeMap);
// 创建求解器配置
// 创建 SolverConfig 对象,并设置求解器配置
SolverConfig solverConfig = new SolverConfig();
solverConfig.setSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
TerminationConfig terminationConfig = new TerminationConfig();
// 运行时长提升效果
// 5s/60s: 技能权重4-技能匹配问题0个,时间窗问题14个
// 10s/300s: 技能权重4-技能匹配问题0个,时间窗问题9个
terminationConfig
.setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit == 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案
terminationConfig.setSecondsSpentLimit(secondsSpentLimit == 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs
solverConfig.withTerminationConfig(terminationConfig);
// 约束条件
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
// 创建求解器
SolverFactory<DispatchSolution> solverFactory = SolverFactory.create(solverConfig);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
solver.addEventListener(new SolverEventListener<DispatchSolution>() {
public void bestSolutionChanged(BestSolutionChangedEvent<DispatchSolution> event) {
// System.out.printf("found better score:%s at time:%ss %n", event.getNewBestScore().toShortString(),
// event.getTimeMillisSpent() / 1000);
System.out.printf("%s,%s%n", event.getNewBestScore().toLevelDoubles()[0],
event.getTimeMillisSpent() / 1000F);
}
});
DispatchSolution solution = solver.solve(problem);
printSolution(solution, customerIndexMap, technicianIndexMap);
System.out.println("final Score: " + solution.getScore().toShortString());
// Create a JacksonSolutionFileIO instance.
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(
DispatchSolution.class);
// Set the output file.
exporter.write(solution, new File("dispatchSolution.json"));
// Obtain a ScoreExplanation object for the best solution
// Using score calculation outside the Solver
// https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
System.out.println(scoreExplanation.getSummary());
// Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
// .getConstraintMatchTotalMap();
// constraintMatchTotalMap.forEach((key, value) -> {
// System.out.println(key + ":" + value.getConstraintName() + ":" + value.getScore().toShortString());
// });
@Override
public Result<?> manualDispatch(long unimprovedSecondsSpentLimit, long secondsSpentLimit)
throws UncheckedIOException, FileNotFoundException {
logger.info("{}", "manual dispatch begin");
// 创建解决方案对象
DispatchSolution problem = DataUtils.getInitialProblem();
// 创建求解器
DefaultSolverFactory<DispatchSolution> solverFactory = DispatchSolutionUtils
.getSolverFactory(unimprovedSecondsSpentLimit, secondsSpentLimit);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
// 得分监听器
solver.addEventListener(new DispatchSolverEventListener());
// 求解
DispatchSolution solution = solver.solve(problem);
return Result.success(solution.getTechnicianList());
}
private static Map<String, Integer> loadCustomerCodeServiceTimeMap()
throws UncheckedIOException, IOException {
List<String> customerServiceTime = IOUtils.readLines(new ClassPathResource("data/customerServiceTime.csv").getInputStream(),
"utf-8");
Map<String, Integer> customerCodeServiceTimeMap = new HashMap<String, Integer>();// code-time
for (int i = 0; i < customerServiceTime.size(); i++) {
String line = customerServiceTime.get(i);
String[] temps = line.split(",");
customerCodeServiceTimeMap.put(temps[0], Integer.parseInt(temps[1]));
}
return customerCodeServiceTimeMap;
}
private static Map<String, Map<String, Long>> loadPreferredlocationDistanceMap()
throws UncheckedIOException, IOException {
List<String> technicianCodeLocation = IOUtils
.readLines(new ClassPathResource("data/technicianLocation.csv").getInputStream(), "utf-8");
Map<String, String> technicianCodeLocationMap = new HashMap<String, String>();// 序号-code
for (int i = 0; i < technicianCodeLocation.size(); i++) {
String line = technicianCodeLocation.get(i);
String[] temps = line.split(",");
technicianCodeLocationMap.put(temps[0], temps[1] + "," + temps[2]);
}
List<String> customerCodeLocation = IOUtils
.readLines(new ClassPathResource("data/customerLocation.csv").getInputStream(), "utf-8");
Map<String, String> customerCodeLocationMap = new HashMap<String, String>();// 序号-code
for (int i = 0; i < customerCodeLocation.size(); i++) {
String line = customerCodeLocation.get(i);
String[] temps = line.split(",");
customerCodeLocationMap.put(temps[0], temps[1] + "," + temps[2]);
}
// 生成订单和技术员的偏好距离Map 技术员-订单-距离
Map<String, Map<String, Long>> tecnicianCustomerDistanceMap = new HashMap<String, Map<String, Long>>();
customerCodeLocationMap.forEach((customerCode, value) -> {
technicianCodeLocationMap.forEach((technicianCode, value2) -> {
String[] temps = RegExUtils.removeAll(value, "\"").split(",");
String[] temps2 = RegExUtils.removeAll(value2, "\"").split(",");
long distance = (long) getDistance(Double.parseDouble(temps[1]), Double.parseDouble(temps[0]),
Double.parseDouble(temps2[1]), Double.parseDouble(temps2[0]));
Map<String, Long> customerMaps = tecnicianCustomerDistanceMap.get(technicianCode);
if (null == customerMaps) {
customerMaps = new HashMap<String, Long>();
tecnicianCustomerDistanceMap.put(technicianCode, customerMaps);
}
customerMaps.put(customerCode, distance);
});
});
return tecnicianCustomerDistanceMap;
}
/**
* 获取经纬度距离
*
* @param lat1 y
* @param lon1 x
* @param lat2 y
* @param lon2 x
* @return
*/
public static double getDistance(double lat1, double lon1, double lat2, double lon2) {
double diffLongitudes = Math.toRadians(Math.abs(lon1 - lon2));
double diffLatitudes = Math.toRadians(Math.abs(lat1 - lat2));
double slat = Math.toRadians(lat1);
double flat = Math.toRadians(lat2);
// haversine formula
double a = Math.sin(diffLatitudes / 2) * Math.sin(diffLatitudes / 2)
+ Math.cos(slat) * Math.cos(flat) * Math.sin(diffLongitudes / 2) * Math.sin(diffLongitudes / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // angular distance in radians
return 6378137 * c;
}
private static Map<String, String> loadCustomerCodeSkillMap() throws UncheckedIOException, IOException {
List<String> customerSkill = IOUtils.readLines(new ClassPathResource("data/customerSkill.csv").getInputStream(), "utf-8");
Map<String, String> customerCodeSkillMap = new HashMap<String, String>();// code-技能
for (int i = 0; i < customerSkill.size(); i++) {
String line = customerSkill.get(i);
String[] temps = line.split(",");
customerCodeSkillMap.put(temps[0], temps[1]);
}
return customerCodeSkillMap;
}
private static Map<String, Set<String>> loadTechnicianCodeSkillsMap()
throws UncheckedIOException, IOException {
List<String> technicianSkills = IOUtils.readLines(new ClassPathResource("data/technicianSkills.csv").getInputStream(), "utf-8");
Map<String, Set<String>> technicianCodeSkillsMap = new HashMap<String, Set<String>>();// code-技能
for (int i = 0; i < technicianSkills.size(); i++) {
String line = technicianSkills.get(i);
String[] temps = line.split(",");
String code = temps[0];
Set<String> skills = technicianCodeSkillsMap.get(code);
if (null == skills) {
skills = new HashSet<>();
technicianCodeSkillsMap.put(code, skills);
}
skills.add(temps[1]);
}
return technicianCodeSkillsMap;
}
private static Map<Integer, String> loadTechnicianIndex() throws UncheckedIOException, IOException {
List<String> technicianIndexlines = IOUtils.readLines(new ClassPathResource("data/technicianIndex.csv").getInputStream(), "utf-8");
Map<Integer, String> technicianIndexMap = new HashMap<Integer, String>();// 序号-code
for (int i = 0; i < technicianIndexlines.size(); i++) {
technicianIndexMap.put(i + 1, technicianIndexlines.get(i));
}
return technicianIndexMap;
}
private static Map<Integer, String> loadCustomerIndex() throws UncheckedIOException, IOException {
List<String> customerIndexlines = IOUtils.readLines(new ClassPathResource("data/customerIndex.csv").getInputStream(), "utf-8");
Map<Integer, String> customerIndexMap = new HashMap<Integer, String>();// 序号-code
for (int i = 0; i < customerIndexlines.size(); i++) {
customerIndexMap.put(i + 1, customerIndexlines.get(i));
}
return customerIndexMap;
}
private static DispatchSolution createVehicleRoutingSolution(Map<Integer, String> customerIndexMap,
Map<Integer, String> technicianIndexMap, Map<String, Set<String>> technicianCodeSkillsMap,
Map<String, String> customerCodeSkillMap, Map<String, Map<String, Long>> preferredlocationDistanceMap, Map<String, Integer> customerCodeServiceTimeMap) throws UncheckedIOException, IOException {
DispatchSolution vehicleRoutingSolution = new DispatchSolution();
// 翻转map
Map<String, Integer> customerIndexMap2 = customerIndexMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
// 初始化距离矩阵
List<String> pathMatrixlines = IOUtils.readLines(new ClassPathResource("data/pathMatrix.csv").getInputStream(), "utf-8");
long[][] pathMatrix = new long[customerIndexMap.keySet().size() + 1][customerIndexMap.keySet().size() + 1];
for (int i = 0; i < pathMatrixlines.size(); i++) {
String line = pathMatrixlines.get(i);
String[] temps = line.split(",");
for (int j = 0; j < temps.length; j++) {
pathMatrix[i][j] = (long) (Float.parseFloat(temps[j]) * 1000);
}
}
Map<Integer, Location> locationIndex = new HashMap<Integer, Location>();
for (int i = 0; i < pathMatrix.length; i++) {
// 1-6
locationIndex.put(i + 1, new Location(i + 1));
}
for (int i = 0; i < pathMatrix.length; i++) {
Location locationi = locationIndex.get(i + 1);
for (int j = 0; j < pathMatrix[i].length; j++) {
Location locationj = locationIndex.get(j + 1);
locationi.getDistanceMap().put(locationj, pathMatrix[i][j]);
}
}
// 初始化时间矩阵
List<String> pathTimeMatrixlines = IOUtils.readLines(new ClassPathResource("data/pathTimeMatrix.csv").getInputStream(), "utf-8");
long[][] pathTimeMatrix = new long[customerIndexMap.keySet().size() + 1][customerIndexMap.keySet().size() + 1];
for (int i = 0; i < pathTimeMatrixlines.size(); i++) {
String line = pathTimeMatrixlines.get(i);
String[] temps = line.split(",");
for (int j = 0; j < temps.length; j++) {
// 秒转分钟
pathTimeMatrix[i][j] = (long) (Math.round(Float.parseFloat(temps[j]) / 60));
}
}
for (int i = 0; i < pathTimeMatrix.length; i++) {
Location locationi = locationIndex.get(i + 1);
for (int j = 0; j < pathTimeMatrix[i].length; j++) {
Location locationj = locationIndex.get(j + 1);
locationi.getDistanceTimeMap().put(locationj, pathTimeMatrix[i][j]);
}
}
// 初始化订单服务窗
List<String> customerWindowslines = IOUtils.readLines(new ClassPathResource("data/customerWindows.csv").getInputStream(), "utf-8");
Map<Integer, Integer> customerStartMap = new HashMap<Integer, Integer>();
Map<Integer, Integer> customerEndMap = new HashMap<Integer, Integer>();
for (int i = 0; i < customerWindowslines.size(); i++) {
String line = customerWindowslines.get(i);
String[] temps = line.split(",");
customerStartMap.put(customerIndexMap2.get(temps[0]), 480 + Integer.parseInt(temps[1]));
customerEndMap.put(customerIndexMap2.get(temps[0]), 480 + Integer.parseInt(temps[2]));
}
// 初始化订单需要技能
Map<Integer, String> customerSkillMap = new HashMap<Integer, String>();
for (int i = 0; i < customerWindowslines.size(); i++) {
// 获取订单技能
customerSkillMap.put(i + 1, customerCodeSkillMap.get(customerIndexMap.get(i + 1)));
if (null == customerCodeSkillMap.get(customerIndexMap.get(i + 1))) {
System.err.printf("%s code:%s 没有技能 %n", i + 1, customerIndexMap.get(i + 1));
System.exit(0);
}
}
// 初始化订单+技能服务时间
List<Customer> customerList = new ArrayList<>();
for (int i = 0; i < customerIndexMap.keySet().size(); i++) {
customerList.add(new Customer(i + 1, customerIndexMap.get(i + 1), locationIndex.get(i + 2),
customerStartMap.get(i + 1), customerEndMap.get(i + 1), customerSkillMap.get(i + 1),
// 初始化技能服务时间
customerCodeServiceTimeMap.get(customerIndexMap.get(i + 1))));
}
// 初始化Depot
Depot depot = new Depot(1, locationIndex.get(1), 480, 1080);
// 初始化技术员
List<Technician> technicianList = new ArrayList<>();
for (int i = 0; i < technicianIndexMap.keySet().size(); i++) {
// 获取第i+1个技术员的技能set
Set<String> skills = technicianCodeSkillsMap.get(technicianIndexMap.get(i + 1));
if (null == skills || skills.size() == 0) {
System.err.printf("技术员%s code:%s 没有技能 %n", i + 1, technicianIndexMap.get(i + 1));
System.exit(0);
}
technicianList.add(new Technician(i + 1, technicianIndexMap.get(i + 1), depot, 480, 1080, skills,
preferredlocationDistanceMap.get(technicianIndexMap.get(i + 1))));
}
vehicleRoutingSolution.setCustomerList(customerList);
vehicleRoutingSolution.setDepot(depot);
vehicleRoutingSolution.setLocationList(new ArrayList<>(locationIndex.values()));
vehicleRoutingSolution.setTechnicianList(technicianList);
return vehicleRoutingSolution;
}
static void printSolution(DispatchSolution solution, Map<Integer, String> customerIndexMap,
Map<Integer, String> technicianIndexMap) {
System.out.println("技能约束:");
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) %s%n", technician.getId(), technicianIndexMap.get((int) technician.getId()),
technician.getSkills());
for (Customer customer : technician.getCustomerList()) {
if (!technician.getSkills().contains(customer.getRequiredSkill())) {
// no match
System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customerIndexMap.get((int) customer.getId()),
customer.getRequiredSkill());
} else {
System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customerIndexMap.get((int) customer.getId()),
customer.getRequiredSkill());
}
}
});
AtomicInteger totalNum = new AtomicInteger(0);
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(),
technicianIndexMap.get((int) technician.getId()), printTime(technician.getStartTime()),
printTime(technician.getEndTime()));
totalNum.addAndGet(technician.getCustomerList().size());
for (Customer customer : technician.getCustomerList()) {
Customer previousCustomer = customer.getPreviousCustomer();
int startPath, endPath;// 路上时间
if (null == previousCustomer) {
startPath = technician.getDepot().getStartTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(technician.getDepot().getLocation());
endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(customer.getLocation());
} else {
startPath = previousCustomer.getDepartureTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(previousCustomer.getLocation());
endPath = startPath + previousCustomer.getLocation().getPathTimeTo(customer.getLocation());
}
System.out.printf(" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customerIndexMap.get((int) customer.getId()),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime()) : "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
customer.getArrivalTime() > customer.getEndTime() ? printTime(customer.getEndTime()) : "",
customer.getArrivalTime() > customer.getEndTime() ? printTime(customer.getArrivalTime()) : "");
}
});
}
private static String printTime(int startTime) {
int hour = startTime / 60;
int minite = startTime % 60;
return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0');
}
}
package com.dituhui.pea.dispatch.utils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RegExUtils;
import com.dituhui.pea.dispatch.pojo.Customer;
import com.dituhui.pea.dispatch.pojo.Depot;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.pojo.Location;
import com.dituhui.pea.dispatch.pojo.Technician;
public class DataUtils {
/**
* 获取初始化测试数据
*
* @return
* @throws UncheckedIOException
* @throws FileNotFoundException
*/
public static DispatchSolution getInitialProblem() throws UncheckedIOException, FileNotFoundException {
Map<Integer, String> customerIndexMap = loadCustomerIndex();
Map<Integer, String> customerIndexXyMap = loadCustomerIndexXY();
Map<Integer, String> technicianIndexMap = loadTechnicianIndex();
Map<String, Set<String>> technicianCodeSkillsMap = loadTechnicianCodeSkillsMap();
Map<String, String> customerCodeSkillMap = loadCustomerCodeSkillMap();
// 偏好中心点位置
Map<String, String> technicianCodePreferredLocationMap = loadPreferredlocationMap();
Map<String, Map<String, Long>> preferredlocationDistanceMap = loadPreferredlocationDistanceMap(
technicianCodePreferredLocationMap);
Map<String, Integer> customerCodeServiceTimeMap = loadCustomerCodeServiceTimeMap();
DispatchSolution problem = createVehicleRoutingSolution(customerIndexMap, customerIndexXyMap,
technicianIndexMap, technicianCodeSkillsMap, customerCodeSkillMap, technicianCodePreferredLocationMap,
preferredlocationDistanceMap, customerCodeServiceTimeMap);
return problem;
}
private static Map<String, String> loadPreferredlocationMap() throws UncheckedIOException, FileNotFoundException {
List<String> technicianCodeLocation = IOUtils.readLines(new FileInputStream("data/technicianLocation.csv"),
"utf-8");
Map<String, String> technicianCodeLocationMap = new HashMap<String, String>();// 序号-code
for (int i = 0; i < technicianCodeLocation.size(); i++) {
String line = technicianCodeLocation.get(i);
String[] temps = line.split(",");
technicianCodeLocationMap.put(temps[0], temps[1] + "," + temps[2]);
}
return technicianCodeLocationMap;
}
private static Map<String, Integer> loadCustomerCodeServiceTimeMap()
throws UncheckedIOException, FileNotFoundException {
List<String> customerServiceTime = IOUtils.readLines(new FileInputStream("data/customerServiceTime.csv"),
"utf-8");
Map<String, Integer> customerCodeServiceTimeMap = new HashMap<String, Integer>();// code-time
for (int i = 0; i < customerServiceTime.size(); i++) {
String line = customerServiceTime.get(i);
String[] temps = line.split(",");
customerCodeServiceTimeMap.put(temps[0], Integer.parseInt(temps[1]));
}
return customerCodeServiceTimeMap;
}
private static Map<String, Map<String, Long>> loadPreferredlocationDistanceMap(
Map<String, String> technicianCodeLocationMap) throws UncheckedIOException, FileNotFoundException {
List<String> customerCodeLocation = IOUtils.readLines(new FileInputStream("data/customerLocation.csv"),
"utf-8");
Map<String, String> customerCodeLocationMap = new HashMap<String, String>();// 序号-code
for (int i = 0; i < customerCodeLocation.size(); i++) {
String line = customerCodeLocation.get(i);
String[] temps = line.split(",");
customerCodeLocationMap.put(temps[0], temps[1] + "," + temps[2]);
}
// 生成订单和技术员的偏好距离Map 技术员-订单-距离
Map<String, Map<String, Long>> customerTecnicianDistanceMap = new HashMap<String, Map<String, Long>>();
customerCodeLocationMap.forEach((customerCode, value) -> {
technicianCodeLocationMap.forEach((technicianCode, value2) -> {
String[] temps = RegExUtils.removeAll(value, "\"").split(",");
String[] temps2 = RegExUtils.removeAll(value2, "\"").split(",");
long distance = (long) getDistance(Double.parseDouble(temps[1]), Double.parseDouble(temps[0]),
Double.parseDouble(temps2[1]), Double.parseDouble(temps2[0]));
Map<String, Long> customerMaps = customerTecnicianDistanceMap.get(technicianCode);
if (null == customerMaps) {
customerMaps = new HashMap<String, Long>();
customerTecnicianDistanceMap.put(technicianCode, customerMaps);
}
customerMaps.put(customerCode, distance);
});
});
return customerTecnicianDistanceMap;
}
/**
* 获取经纬度距离
*
* @param lat1 y
* @param lon1 x
* @param lat2 y
* @param lon2 x
* @return
*/
private static double getDistance(double lat1, double lon1, double lat2, double lon2) {
double diffLongitudes = Math.toRadians(Math.abs(lon1 - lon2));
double diffLatitudes = Math.toRadians(Math.abs(lat1 - lat2));
double slat = Math.toRadians(lat1);
double flat = Math.toRadians(lat2);
// haversine formula
double a = Math.sin(diffLatitudes / 2) * Math.sin(diffLatitudes / 2)
+ Math.cos(slat) * Math.cos(flat) * Math.sin(diffLongitudes / 2) * Math.sin(diffLongitudes / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // angular distance in radians
return 6378137 * c;
}
private static Map<String, String> loadCustomerCodeSkillMap() throws UncheckedIOException, FileNotFoundException {
List<String> customerSkill = IOUtils.readLines(new FileInputStream("data/customerSkill.csv"), "utf-8");
Map<String, String> customerCodeSkillMap = new HashMap<String, String>();// code-技能
for (int i = 0; i < customerSkill.size(); i++) {
String line = customerSkill.get(i);
String[] temps = line.split(",");
customerCodeSkillMap.put(temps[0], temps[1]);
}
return customerCodeSkillMap;
}
private static Map<String, Set<String>> loadTechnicianCodeSkillsMap()
throws UncheckedIOException, FileNotFoundException {
List<String> technicianSkills = IOUtils.readLines(new FileInputStream("data/technicianSkills.csv"), "utf-8");
Map<String, Set<String>> technicianCodeSkillsMap = new HashMap<String, Set<String>>();// code-技能
for (int i = 0; i < technicianSkills.size(); i++) {
String line = technicianSkills.get(i);
String[] temps = line.split(",");
String code = temps[0];
Set<String> skills = technicianCodeSkillsMap.get(code);
if (null == skills) {
skills = new HashSet<>();
technicianCodeSkillsMap.put(code, skills);
}
skills.add(temps[1]);
}
return technicianCodeSkillsMap;
}
private static Map<Integer, String> loadTechnicianIndex() throws UncheckedIOException, FileNotFoundException {
List<String> technicianIndexlines = IOUtils.readLines(new FileInputStream("data/technicianIndex.csv"), "utf-8");
Map<Integer, String> technicianIndexMap = new HashMap<Integer, String>();// 序号-code
for (int i = 0; i < technicianIndexlines.size(); i++) {
technicianIndexMap.put(i + 1, technicianIndexlines.get(i));
}
return technicianIndexMap;
}
private static Map<Integer, String> loadCustomerIndex() throws UncheckedIOException, FileNotFoundException {
List<String> customerIndexlines = IOUtils.readLines(new FileInputStream("data/customerIndex.csv"), "utf-8");
Map<Integer, String> customerIndexMap = new HashMap<Integer, String>();// 序号-code
for (int i = 0; i < customerIndexlines.size(); i++) {
customerIndexMap.put(i + 1, customerIndexlines.get(i));
}
return customerIndexMap;
}
private static Map<Integer, String> loadCustomerIndexXY() throws UncheckedIOException, FileNotFoundException {
List<String> customerCodeXYlines = IOUtils.readLines(new FileInputStream("data/customerIndexXY.csv"), "utf-8");
Map<Integer, String> customerIndexXYMap = new HashMap<Integer, String>();// 序号-code
for (int i = 0; i < customerCodeXYlines.size(); i++) {
String line = customerCodeXYlines.get(i);
String[] temps = line.split(",");
customerIndexXYMap.put(i + 1,
RegExUtils.removeAll(temps[1], "\"") + "," + RegExUtils.removeAll(temps[2], "\""));
}
return customerIndexXYMap;
}
private static DispatchSolution createVehicleRoutingSolution(Map<Integer, String> customerIndexMap,
Map<Integer, String> customerIndexXyMap, Map<Integer, String> technicianIndexMap,
Map<String, Set<String>> technicianCodeSkillsMap, Map<String, String> customerCodeSkillMap,
Map<String, String> technicianCodePreferredLocationMap,
Map<String, Map<String, Long>> preferredlocationDistanceMap,
Map<String, Integer> customerCodeServiceTimeMap) throws UncheckedIOException, FileNotFoundException {
DispatchSolution vehicleRoutingSolution = new DispatchSolution();
// 翻转map
Map<String, Integer> customerIndexMap2 = customerIndexMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
// 初始化距离矩阵
List<String> pathMatrixlines = IOUtils.readLines(new FileInputStream("data/pathMatrix.csv"), "utf-8");
long[][] pathMatrix = new long[customerIndexMap.keySet().size() + 1][customerIndexMap.keySet().size() + 1];
for (int i = 0; i < pathMatrixlines.size(); i++) {
String line = pathMatrixlines.get(i);
String[] temps = line.split(",");
for (int j = 0; j < temps.length; j++) {
pathMatrix[i][j] = (long) (Float.parseFloat(temps[j]) * 1000);
}
}
Map<Integer, Location> locationIndex = new HashMap<Integer, Location>();
for (int i = 0; i < pathMatrix.length; i++) {
// 1 ~ N+1
String xyString = customerIndexXyMap.get(i + 1);
String[] temps = xyString.split(",");
locationIndex.put(i + 1, new Location(i + 1, Float.parseFloat(temps[0]), Float.parseFloat(temps[1])));
}
for (int i = 0; i < pathMatrix.length; i++) {
Location locationi = locationIndex.get(i + 1);
for (int j = 0; j < pathMatrix[i].length; j++) {
Location locationj = locationIndex.get(j + 1);
locationi.getDistanceMap().put(locationj, pathMatrix[i][j]);
}
}
// 初始化时间矩阵
List<String> pathTimeMatrixlines = IOUtils.readLines(new FileInputStream("data/pathTimeMatrix.csv"), "utf-8");
long[][] pathTimeMatrix = new long[customerIndexMap.keySet().size() + 1][customerIndexMap.keySet().size() + 1];
for (int i = 0; i < pathTimeMatrixlines.size(); i++) {
String line = pathTimeMatrixlines.get(i);
String[] temps = line.split(",");
for (int j = 0; j < temps.length; j++) {
// 秒转分钟
pathTimeMatrix[i][j] = (long) (Math.round(Float.parseFloat(temps[j]) / 60));
}
}
for (int i = 0; i < pathTimeMatrix.length; i++) {
Location locationi = locationIndex.get(i + 1);
for (int j = 0; j < pathTimeMatrix[i].length; j++) {
Location locationj = locationIndex.get(j + 1);
locationi.getDistanceTimeMap().put(locationj, pathTimeMatrix[i][j]);
}
}
// 初始化订单服务窗
List<String> customerWindowslines = IOUtils.readLines(new FileInputStream("data/customerWindows.csv"), "utf-8");
Map<Integer, Integer> customerStartMap = new HashMap<Integer, Integer>();
Map<Integer, Integer> customerEndMap = new HashMap<Integer, Integer>();
for (int i = 0; i < customerWindowslines.size(); i++) {
String line = customerWindowslines.get(i);
String[] temps = line.split(",");
customerStartMap.put(customerIndexMap2.get(temps[0]), 480 + Integer.parseInt(temps[1]));
customerEndMap.put(customerIndexMap2.get(temps[0]), 480 + Integer.parseInt(temps[2]));
}
// 初始化订单需要技能
Map<Integer, String> customerSkillMap = new HashMap<Integer, String>();
for (int i = 0; i < customerWindowslines.size(); i++) {
// 获取订单技能
customerSkillMap.put(i + 1, customerCodeSkillMap.get(customerIndexMap.get(i + 1)));
if (null == customerCodeSkillMap.get(customerIndexMap.get(i + 1))) {
System.err.printf("%s code:%s 没有技能 %n", i + 1, customerIndexMap.get(i + 1));
System.exit(0);
}
}
// 初始化订单+技能服务时间
List<Customer> customerList = new ArrayList<>();
for (int i = 0; i < customerIndexMap.keySet().size(); i++) {
customerList.add(new Customer(i + 1, customerIndexMap.get(i + 1), locationIndex.get(i + 2),
customerStartMap.get(i + 1), customerEndMap.get(i + 1), customerSkillMap.get(i + 1),
// 初始化技能服务时间
customerCodeServiceTimeMap.get(customerIndexMap.get(i + 1))));
}
// 初始化Depot
Depot depot = new Depot(1, locationIndex.get(1), 480, 1080);
// 初始化技术员
List<Technician> technicianList = new ArrayList<>();
for (int i = 0; i < technicianIndexMap.keySet().size(); i++) {
// 获取第i+1个技术员的技能set
Set<String> skills = technicianCodeSkillsMap.get(technicianIndexMap.get(i + 1));
if (null == skills || skills.size() == 0) {
System.err.printf("技术员%s code:%s 没有技能 %n", i + 1, technicianIndexMap.get(i + 1));
System.exit(0);
}
String xyString = technicianCodePreferredLocationMap.get(technicianIndexMap.get(i + 1));
String[] temps = xyString.split(",");
Location preferredlocation = new Location(i + 1, Float.parseFloat(RegExUtils.removeAll(temps[0], "\"")),
Float.parseFloat(RegExUtils.removeAll(temps[1], "\"")));
technicianList.add(new Technician(i + 1, technicianIndexMap.get(i + 1), depot, 480, 1080, skills,
preferredlocationDistanceMap.get(technicianIndexMap.get(i + 1)), preferredlocation));
}
vehicleRoutingSolution.setCustomerList(customerList);
vehicleRoutingSolution.setDepot(depot);
vehicleRoutingSolution.setLocationList(new ArrayList<>(locationIndex.values()));
vehicleRoutingSolution.setTechnicianList(technicianList);
return vehicleRoutingSolution;
}
}
package com.dituhui.pea.dispatch.utils;
import static java.util.Comparator.comparing;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.optaplanner.constraint.streams.drools.DroolsConstraintStreamScoreDirector;
import org.optaplanner.core.api.score.ScoreExplanation;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.core.api.score.constraint.ConstraintMatch;
import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal;
import org.optaplanner.core.api.solver.SolutionManager;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.config.score.director.ScoreDirectorFactoryConfig;
import org.optaplanner.core.config.solver.SolverConfig;
import org.optaplanner.core.config.solver.termination.TerminationConfig;
import org.optaplanner.core.impl.domain.variable.descriptor.ListVariableDescriptor;
import org.optaplanner.core.impl.heuristic.selector.move.generic.list.ListChangeMove;
import org.optaplanner.core.impl.score.director.ScoreDirectorFactory;
import org.optaplanner.core.impl.solver.DefaultSolverFactory;
import org.optaplanner.persistence.jackson.impl.domain.solution.JacksonSolutionFileIO;
import com.dituhui.pea.dispatch.constraint.ConstraintNameEnum;
import com.dituhui.pea.dispatch.constraint.DispatchConstraintProvider;
import com.dituhui.pea.dispatch.pojo.Customer;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.pojo.Technician;
public class DispatchSolutionUtils {
/**
* 打印方案
*
* @param solution
*/
public static void printSolution(DispatchSolution solution) {
System.out.println("Score: " + solution.getScore().toShortString());
System.out.println("技能约束:");
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) %s%n", technician.getId(), technician.getCode(), technician.getSkills());
for (Customer customer : technician.getCustomerList()) {
if (!technician.getSkills().contains(customer.getRequiredSkill())) {
// no match
System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
} else {
System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill());
}
}
});
AtomicInteger totalNum = new AtomicInteger(0);
solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(), technician.getCode(),
printTime(technician.getStartTime()), printTime(technician.getEndTime()));
totalNum.addAndGet(technician.getCustomerList().size());
for (Customer customer : technician.getCustomerList()) {
Customer previousCustomer = customer.getPreviousCustomer();
int startPath, endPath;// 路上时间
if (null == previousCustomer) {
startPath = technician.getDepot().getStartTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(technician.getDepot().getLocation());
endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(customer.getLocation());
} else {
startPath = previousCustomer.getDepartureTime();
// endPath = startPath +
// customer.getLocation().getPathTimeTo(previousCustomer.getLocation());
endPath = startPath + previousCustomer.getLocation().getPathTimeTo(customer.getLocation());
}
System.out.printf(" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(),
// 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间
printTime(startPath), printTime(endPath),
// 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime()) : "",
// 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间
customer.getArrivalTime() > customer.getEndTime() ? printTime(customer.getEndTime()) : "",
customer.getArrivalTime() > customer.getEndTime() ? printTime(customer.getArrivalTime()) : "");
}
});
}
private static String printTime(int startTime) {
int hour = startTime / 60;
int minite = startTime % 60;
return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0');
}
/**
* 生成方案地图页
*
* @param solution
* @param filename
*/
public static void exportMapHtml(DispatchSolution solution, String filename) {
try {
// 仓库起点
String depot = "\"" + solution.getDepot().getLocation().getX() + ","
+ solution.getDepot().getLocation().getY() + "\"";
// 技术员路线
String lines_ = "[";
for (Technician technician : solution.getTechnicianList()) {
lines_ += "\"" + technician.getCustomerList().stream()
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY()).reduce((a, b) -> a + ";" + b)
.get() + "\",";
}
lines_ += "]";
final String lines = lines_;
// 技术员偏好中心点
String preferredlocation = "\"" + solution.getTechnicianList().stream()
.map(c -> c.getPreferredlocation().getX() + "," + c.getPreferredlocation().getY())
.reduce((a, b) -> a + ";" + b).get() + "\"";
List<String> dispatchMapLines = IOUtils.readLines(new FileInputStream("data/dispatchMap.html"), "GBK");
dispatchMapLines = dispatchMapLines.stream().map(line -> {
if (StringUtils.startsWith(line, " var depot = ")) {
return " var depot = " + depot;
} else if (StringUtils.startsWith(line, " var preferredlocation = ")) {
return " var preferredlocation = " + preferredlocation;
} else if (StringUtils.startsWith(line, " var lines = ")) {
return " var lines = " + lines;
} else {
return line;
}
}).collect(Collectors.toList());
IOUtils.writeLines(dispatchMapLines, "\r\n", new FileOutputStream(filename + ".html"), "GBK");
System.out.println("output map : " + filename);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 计算方案分数
*
* @param solution
* @return
*/
public static HardSoftLongScore getScore(DispatchSolution solution) {
DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory();
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Set the working solution of the ScoreDirector object.
scoreDirector.setWorkingSolution(solution);
// Iterate over the constraints in your problem and call the
// `addConstraintMatch()` method
// for each constraint that is violated by the solution.
// for (Constraint constraint : myConstraints) {
// if (constraint.isViolated(mySolution)) {
// scoreDirector.addConstraintMatch(constraint, constraint.getScore());
// }
// }
// Calculate the score of the solution.
HardSoftLongScore score = (HardSoftLongScore) scoreDirector.calculateScore();
return score;
}
/**
* 移动某个技术员的订单,返回新方案
*
* @param solution
* @param source 技术员
* @param sourceIndex 0开始
* @param destinationIndex 0开始
* @return
*/
public static DispatchSolution moveCustomer(DefaultSolverFactory<DispatchSolution> solverFactory,
DispatchSolution solution, Technician source, int sourceIndex, int destinationIndex) {
if (null == solverFactory) {
solverFactory = getSolverFactory();
}
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector();
// Make a change to the solution.
// doMove(), doSwap(), or doChangeVariable()
// Create a variable descriptor for the "customerList" list property
ListVariableDescriptor<DispatchSolution> customerDescriptor = (ListVariableDescriptor<DispatchSolution>) scoreDirector
.getSolutionDescriptor().getEntityDescriptorStrict(Technician.class)
.getVariableDescriptor("customerList");
// Create the ListChangeMove
ListChangeMove<DispatchSolution> move = new ListChangeMove<DispatchSolution>(customerDescriptor, source,
sourceIndex, source, destinationIndex);
// apply the move and re-evaluate the score
scoreDirector.doAndProcessMove(move, true);
return scoreDirector.getWorkingSolution();
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory() {
return getSolverFactory(0, 0);
}
public static DefaultSolverFactory<DispatchSolution> getSolverFactory(long unimprovedSecondsSpentLimit,
long secondsSpentLimit) {
// 创建求解器配置
// 创建 SolverConfig 对象,并设置求解器配置
SolverConfig solverConfig = new SolverConfig();
solverConfig.setSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
TerminationConfig terminationConfig = new TerminationConfig();
terminationConfig
.setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit <= 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案
terminationConfig.setSecondsSpentLimit(secondsSpentLimit <= 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs
solverConfig.withTerminationConfig(terminationConfig);
// 约束条件
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.setScoreDirectorFactoryConfig(
new ScoreDirectorFactoryConfig().withConstraintProviderClass(DispatchConstraintProvider.class));
DefaultSolverFactory<DispatchSolution> solverFactory = (DefaultSolverFactory<DispatchSolution>) SolverFactory
.<DispatchSolution>create(solverConfig);
return solverFactory;
}
/**
* 打印详细约束得分
*
* @param solution
*/
public static void explainSolutionConstraintDetail(DispatchSolution solution) {
DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory();
// Obtain a ScoreExplanation object for the best solution
// Using score calculation outside the Solver
// https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
// System.out.println(scoreExplanation.getSummary());
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) {
// 违反硬约束
System.out.printf("%s 匹配%s次 hard得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().hardScore());
} else {
// 软约束
System.out.printf("%s 匹配%s次 soft得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().softScore());
}
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> {
String text = "";
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch:
case customerTimeWindowsMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject;
text += customer.getCode() + ",";
}
}
System.out.printf(" 预约单(%s)违反约束,扣分%s%n", text, constraintMatch.getScore().toShortString());
break;
case totalDistance:
case preferredTotalDistance:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)总路程得分%s%n", text, constraintMatch.getScore().toShortString());
break;
case technicianBalanceSoft:
if (constraintMatch.getScore().softScore() < 0) {
for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject;
text += technician.getCode() + ",";
}
}
System.out.printf(" 技术员(%s)订单量差距得分%s%n", text,
constraintMatch.getScore().toShortString());
}
break;
default:
break;
}
});
});
}
/**
* 导出json结构
*
* @param solution
* @param fileName
*/
public static void exportSolutionJson(DispatchSolution solution, String fileName) {
// Create a JacksonSolutionFileIO instance.
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(
DispatchSolution.class);
// Set the output file.
exporter.write(solution, new File(fileName));
}
}
......@@ -26,7 +26,7 @@ spring:
enabled: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.10.0.116:3306/saas_aftersale_test?serverTimezone=Asia/Shanghai
url: jdbc:mysql://10.10.0.54:3306/saas_aftersale_test?serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
......
depot,"120.614853,31.338807"
106330400,"120.982906,31.440902"
106632743,"120.726916,31.592422"
106632993,"120.868246,31.041383"
106704506,"120.625144,31.257837"
106729637,"120.959821,31.402083"
106732179,"121.077572,31.563578"
106737923,"120.622214,31.333417"
106758769,"120.731802,31.300809"
106765790,"120.847507,31.42479"
106790276,"120.606255,31.268432"
106798348,"120.959821,31.402083"
106845767,"120.687214,31.253326"
106848936,"120.679366,31.287809"
106854117,"120.664649,31.316367"
106855369,"120.616312,31.385716"
106865528,"120.623197,31.470323"
106867266,"120.597917,31.348755"
106879505,"120.765083,31.697774"
106882180,"120.664487,31.282887"
106893582,"121.107535,31.479541"
106228451,"120.721813,31.303003"
106660472,"120.611127,31.250817"
106684680,"120.626474,31.224566"
106703025,"120.530752,31.281024"
106762173,"120.606634,31.292016"
106802404,"120.654614,31.098804"
106820041,"120.719909,31.382979"
106820735,"120.58446,31.322863"
106824266,"120.697424,31.344814"
106828070,"120.596177,31.283586"
106833813,"120.523634,31.858481"
106835282,"121.039728,31.400563"
106853363,"121.104428,31.453363"
106856986,"120.745434,31.304664"
106861402,"120.785598,31.097726"
106863823,"120.966152,31.389332"
106870737,"120.612394,31.194301"
106870765,"120.451477,31.938889"
106871040,"120.686309,31.327041"
106872591,"120.591992,31.292224"
106872658,"120.768159,31.337805"
106875426,"120.64272,31.277918"
106890613,"120.526677,31.863775"
106893208,"120.748525,31.324368"
106279591,"120.982906,31.440902"
106704564,"120.625144,31.257837"
106729640,"120.959821,31.402083"
106732181,"121.077572,31.563578"
106738036,"120.622214,31.333417"
106758988,"120.731802,31.300809"
106765761,"120.847507,31.42479"
106818668,"120.493618,31.24607"
106823211,"120.651719,31.278747"
106824512,"120.754292,31.337413"
106836405,"120.665415,31.32013"
106841840,"120.615365,31.186788"
106851428,"120.98225,31.417492"
106852112,"120.654228,31.337012"
106853178,"120.632811,31.173964"
106854359,"120.84248,31.284855"
106855674,"120.743073,31.677162"
106856390,"120.655769,31.393115"
106859149,"120.724624,31.487646"
106859862,"120.549426,31.376686"
106860870,"120.748602,31.317585"
106862449,"120.777722,31.636159"
106865892,"120.664487,31.282887"
106866730,"120.606376,31.391923"
106868422,"120.702768,31.333509"
106870053,"120.732045,31.30597"
106873767,"120.964531,31.349257"
106873992,"120.574297,31.321478"
106876557,"120.721418,31.487407"
106876682,"121.088316,31.5656"
106881724,"120.601868,31.148427"
106883231,"121.120276,31.465135"
106883391,"120.861145,31.080919"
106888947,"120.628533,31.368943"
106889389,"120.680358,31.325299"
106893581,"121.107535,31.479541"
106776742,"120.70851,31.248646"
106820178,"120.678376,31.149701"
106826035,"120.49468,31.996066"
106835053,"120.600039,31.295101"
106839639,"120.883187,31.287842"
106840295,"120.526884,31.373801"
106848186,"121.135103,31.441649"
106869743,"120.938132,31.396139"
106871570,"120.634245,31.188141"
106871918,"120.64205,31.296388"
106872059,"120.589301,31.3104"
106874727,"120.748439,31.684466"
106879292,"120.715751,31.29945"
106881156,"120.566187,31.855965"
106884799,"120.605229,31.394392"
106886425,"120.548647,31.842596"
106890832,"120.604399,31.349966"
106576847,"121.106111,31.480717"
106704602,"120.625144,31.257837"
106729639,"120.959821,31.402083"
106732180,"121.077572,31.563578"
106738035,"120.622214,31.333417"
106853161,"120.632811,31.173964"
106855710,"120.743073,31.677162"
106856389,"120.655769,31.393115"
106859148,"120.724624,31.487646"
106860869,"120.748602,31.317585"
106862447,"120.777722,31.636159"
106866729,"120.606376,31.391923"
106868053,"120.606634,31.292016"
106888946,"120.628533,31.368943"
106854169,"120.634382,31.374593"
106870713,"120.633518,31.153463"
106891293,"120.636053,31.289328"
106549827,"121.144142,31.422735"
106765447,"120.616316,31.402105"
106781641,"120.606019,31.398212"
106793056,"120.747643,31.310646"
106803268,"120.607062,31.398126"
106818600,"121.144926,31.461553"
106822716,"120.705113,31.257879"
106829323,"120.673489,31.297058"
106840366,"120.649177,31.303238"
106847687,"120.530373,31.853617"
106866881,"120.629969,31.270552"
106870492,"120.650458,31.225728"
106871200,"120.59747,31.285217"
106872822,"120.616482,31.402948"
106878500,"120.606019,31.398212"
106880124,"120.615837,31.316617"
106880969,"120.677186,31.149534"
106892125,"120.587065,31.283529"
106763043,"120.687676,31.443469"
106801344,"120.69381,31.304455"
106806874,"120.650618,31.176803"
106824742,"120.77035,31.312694"
106858813,"120.769821,31.342065"
106870383,"120.624333,31.361689"
106632622,"120.726916,31.592422"
106744960,"121.077777,31.560303"
106809648,"120.956653,31.459789"
106862554,"120.685176,31.471252"
106649991,"120.632112,31.286827"
106753590,"120.597212,31.294007"
106845946,"120.747488,31.340815"
106859379,"120.756575,31.336275"
106632543,"120.726916,31.592422"
106632849,"120.726916,31.592422"
106665842,"120.64402,31.77468"
106730931,"120.639926,31.306425"
106744959,"121.077777,31.560303"
106809662,"120.956653,31.459789"
106813589,"120.844551,31.013903"
106862553,"120.685176,31.471252"
106871868,"120.686193,31.327165"
106879520,"120.614752,31.309742"
106694001,"120.632112,31.286827"
106828628,"120.77099,31.682885"
106834398,"120.761397,31.311754"
106886055,"120.598933,31.297181"
106665727,"120.64402,31.77468"
106665740,"120.64402,31.77468"
106704534,"120.625144,31.257837"
106809681,"120.956653,31.459789"
106862552,"120.685176,31.471252"
106884214,"120.620985,31.129614"
106884485,"120.485939,31.334295"
106885700,"120.727943,31.254621"
106887147,"120.790707,31.683097"
106754377,"120.639734,31.32923"
106775190,"120.612111,31.189908"
106849556,"120.605103,31.296943"
106867319,"120.696831,31.275636"
106888973,"120.898629,31.749332"
106889146,"120.60652,31.404533"
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk">
<title>算法测试</title>
<script type="text/javascript" src="http://webapi.amap.com/maps?v=1.4.15&key=b64407a9a60c945f80270b17f9c4eabd"></script>
<style>
#iMap {
height: 560px;
width: 100%;
float: left;
}
.info {
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border-radius: .25rem;
position: fixed;
top: 1rem;
background-color: white;
width: auto;
min-width: 22rem;
border-width: 0;
right: 1rem;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
}
label {
width: 80px;
float: left;
}
.detail {
padding: 10px;
border: 1px solid #aaccaa;
}
textarea {
width: 650px;
height: 100px;
}
.input-card {
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border-radius: .25rem;
width: 22rem;
border-width: 0;
border-radius: 0.4rem;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
position: fixed;
bottom: 1rem;
right: 1rem;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
padding: 0.75rem 1.25rem;
}
</style>
</head>
<body onload="mapInit()">
<div id="iMap"></div>
<div class="input-card" style="width:19rem">
<h4>设置地图显示要素(Features)</h4>
<div id="map-features">
<div class="input-item">
<input type='checkbox' name='mapStyle' value='bg' checked>
<span class="input-text">区域面(bg)</span>
</div>
<div class="input-item">
<input type='checkbox' name='mapStyle' value='road' checked>
<span class="input-text">道路(road)</span>
</div>
<div class="input-item">
<input type='checkbox' name='mapStyle' value='building' checked>
<span class="input-text">建筑物(building)</span>
</div>
<div class="input-item">
<input type='checkbox' name='mapStyle' value='point' checked>
<span class="input-text">标注(point)</span>
</div>
</div>
</div>
</body>
<script language="javascript">
var mapObj;
var lnglatXY;
var polygon;
//初始化地图
function mapInit() {
var opt = {
center: [120.61485,31.338806],
zoom: 10
}
mapObj = new AMap.Map("iMap", opt);
// 技术员路线
var colors = ["6EA7C1","6B55AE","E67FA2","FFEFA1","000000","4E4FEB","068FFF","EEEEEE","F2D8D8","5C8984","545B77","374259","525FE1","F86F03","FFA41B","FFF6F4","E90064","B3005E","FF5F9E","F29727"];
var lines = ["120.76816,31.337805;120.697426,31.344814;120.68631,31.327042;120.678375,31.1497;120.64272,31.277918;120.606636,31.292015;120.59893,31.29718;120.59721,31.294006;120.64205,31.296389;120.63973,31.32923;120.62434,31.361689;120.636055,31.289328","120.6044,31.349966;120.70851,31.248646;120.65462,31.098804;120.7856,31.097727;120.93813,31.39614;120.96615,31.389332;121.1351,31.441648;121.10443,31.453363;121.03973,31.400562;120.89863,31.749332","120.731804,31.30081;120.61632,31.402105;120.622215,31.333418;121.10754,31.479542;120.68517,31.471252;120.60602,31.398212;120.61584,31.316616","120.95982,31.402082;120.95665,31.45979;120.61631,31.385715;120.66465,31.316366;120.95982,31.402082;120.54942,31.376686","121.077576,31.563578;121.077774,31.560303;120.777725,31.63616;120.574295,31.321478;120.48594,31.334295;120.66449,31.282887;120.606636,31.292015;120.63281,31.173964;120.59747,31.285217","120.687675,31.443468;120.748436,31.684465;120.566185,31.855965;120.49468,31.996065;120.45148,31.938889;120.52668,31.863775;120.523636,31.85848;120.548645,31.842596;120.77099,31.682884;120.60523,31.394392","120.615364,31.186789;120.606255,31.268433;120.95982,31.402082;121.14493,31.461554;120.70512,31.25788;120.74764,31.310646;120.84248,31.284855","120.58707,31.28353;120.53037,31.853617;120.68722,31.253326;120.62997,31.270552","120.5893,31.3104;120.6051,31.296944;120.591995,31.292225;120.596176,31.283587;120.63211,31.286827;120.60004,31.295101;120.58446,31.322863;120.69683,31.275637;120.71991,31.382978;120.76982,31.342066;120.60652,31.404533","121.14414,31.422735;121.10754,31.479542;121.12028,31.465136;120.861145,31.08092;120.724625,31.487646;120.60187,31.148426","120.65577,31.393114;120.79071,31.683098;120.74307,31.677162;120.76508,31.697775;120.74307,31.677162;120.95982,31.402082;120.8475,31.42479;120.622215,31.333418;120.66449,31.282887","120.65423,31.337011;120.67349,31.297058;120.64918,31.303238;120.748604,31.317585;120.65577,31.393114;120.72142,31.487408;120.622215,31.333418;120.62099,31.129614;120.65046,31.225727","120.754295,31.337414;120.625145,31.257837;120.625145,31.257837;120.65172,31.278748;120.625145,31.257837;120.63281,31.173964;120.86825,31.041384;120.9829,31.440903;120.96453,31.349257","120.625145,31.257837;120.493614,31.24607;120.60706,31.398127;120.616486,31.402948;120.62853,31.368942;120.66541,31.32013;120.677185,31.149534;120.731804,31.30081","120.530754,31.281025;120.61113,31.250816;120.62647,31.224566;120.6124,31.194302;120.63425,31.18814;120.612114,31.189907;120.633514,31.153463;120.526886,31.3738;120.634384,31.374594;120.75658,31.336275;120.65062,31.176804","120.63211,31.286827;120.69381,31.304455;120.74544,31.304665;120.74749,31.340815;120.74853,31.324368;120.77035,31.312695;120.7614,31.311754;120.71575,31.29945;120.72181,31.303003;120.88319,31.287842","120.60638,31.391924;120.748604,31.317585;120.67937,31.28781;120.8475,31.42479;120.68036,31.325298;121.077576,31.563578;121.08832,31.5656;121.077576,31.563578;120.62853,31.368942","120.64402,31.77468;120.64402,31.77468;120.597916,31.348755;120.60638,31.391924;120.6232,31.470324;120.72691,31.592422;120.72691,31.592422;120.724625,31.487646;120.72794,31.254622","120.777725,31.63616;120.60602,31.398212;120.70277,31.33351;120.73205,31.30597;120.95665,31.45979;121.10611,31.480717;120.9829,31.440903;120.98225,31.417492;120.68517,31.471252","120.72691,31.592422;120.84455,31.013903;120.95665,31.45979;121.077774,31.560303;120.72691,31.592422;120.68517,31.471252;120.63992,31.306425;120.686195,31.327166;120.61475,31.309742;120.64402,31.77468"];
lines.forEach(function(line,index){
showLine(line,colors[index]);
});
// 仓库起点
var depot = "120.61485,31.338806";
addMarker2(depot);
// 技术员偏好中心点
var preferredlocation = "120.753876,31.312792;120.60937,31.37701;120.59335,31.314192;120.6781,31.312456;120.694275,31.230944;120.68786,31.631952;120.62794,31.25647;120.630035,31.153814;120.53737,31.36919;120.597626,31.18977;120.62713,31.29919;120.69743,31.234127;120.634636,31.298191;120.72335,31.296595;120.39913,31.233416;120.72677,31.3044;120.66996,31.272655;120.6085,31.126905;120.385025,31.351074;120.736176,31.337831";
preferredlocation.split(';').forEach(function(xy,index){
addCircle(xy,colors[index]);
});
}
// 画线
// 多边形轮廓线的节点坐标数组
function showLine(bounds,color) {
if ('' == bounds) {
alert("请输入坐标串");
return;
}
var path = bounds.split(';').map(xy => {
var segs = xy.split(',');
return new AMap.LngLat(segs[0], segs[1]);
});
// alert(path.length + "个坐标");
var polygon = new AMap.Polyline({
path: path,
strokeColor: '#'+color, // 颜色
strokeWeight: 5, // 线条宽度,默认为 1
strokeOpacity: 0.8, //线透明度
strokeStyle: 'solid', // 线条样式
showDir: true, //白色方向箭头
});
mapObj.add(polygon);
}
// 实例化点标记
function addMarker(bounds,image) {
//var bounds = document.getElementById("markers").value;
if ('' == bounds) {
alert("请输入坐标串");
return;
}
bounds.split(';').map(xy => {
var segs = xy.split(',');
var marker = new AMap.Marker({
icon: image,
position: [segs[0], segs[1]],
//anchor: 'bottom-center', //设置锚点
//offset: new AMap.Pixel(-13, -30)
});
marker.setMap(mapObj);
return new AMap.LngLat(segs[0], segs[1]);
});
}
// 实例化点标记
function addMarker2(bounds) {
if ('' == bounds) {
alert("请输入坐标串");
return;
}
// 创建一个 Icon
var startIcon = new AMap.Icon({
// 图标尺寸
size: new AMap.Size(25, 34),
// 图标的取图地址
image: 'http://a.amap.com/jsapi_demos/static/demo-center/icons/dir-marker.png',
// 图标所用图片大小
imageSize: new AMap.Size(135, 40),
// 图标取图偏移量
imageOffset: new AMap.Pixel(-9, -3)
});
bounds.split(';').map(xy => {
var segs = xy.split(',');
var marker = new AMap.Marker({
icon: startIcon,
position: [segs[0], segs[1]],
//anchor: 'bottom-center', //设置锚点
offset: new AMap.Pixel(-13, -30)
});
marker.setMap(mapObj);
return new AMap.LngLat(segs[0], segs[1]);
});
}
// 实例化圆形标记
function addCircle(bounds,color) {
if ('' == bounds) {
alert("请输入坐标串");
return;
}
bounds.split(';').map(xy => {
var segs = xy.split(',');
// 构造矢量圆形
var circle = new AMap.Circle({
center: new AMap.LngLat(segs[0], segs[1]), // 圆心位置
radius: 2000, //半径
strokeColor: "#"+color, //线颜色
strokeOpacity: 1, //线透明度
strokeWeight: 3, //线粗细度
fillColor: "#"+color, //填充颜色
fillOpacity: 0.8 //填充透明度
});
mapObj.add(circle);
return new AMap.LngLat(segs[0], segs[1]);
});
}
//设置地图显示要素
function setMapFeatures() {
var features = [];
var inputs = document.querySelectorAll("#map-features input");
inputs.forEach(function(input) {
if (input.checked) {
features.push(input.value);
}
});
mapObj.setFeatures(features);
}
//绑定checkbox点击事件
var inputs = document.querySelectorAll("#map-features input");
inputs.forEach(function(checkbox) {
checkbox.onclick = setMapFeatures;
});
</script>
</html>
\ No newline at end of file
package com.dituhui.pea.dispatch.test;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.impl.solver.DefaultSolverFactory;
import com.dituhui.pea.dispatch.eventListener.DispatchSolverEventListener;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.utils.DataUtils;
import com.dituhui.pea.dispatch.utils.DispatchSolutionUtils;
public class Test {
public static void main(String[] args) throws UncheckedIOException, FileNotFoundException {
// 创建解决方案对象
DispatchSolution problem = DataUtils.getInitialProblem();
// 创建求解器
DefaultSolverFactory<DispatchSolution> solverFactory = DispatchSolutionUtils.getSolverFactory(3, 30);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
// 得分监听器
solver.addEventListener(new DispatchSolverEventListener());
// 求解
DispatchSolution solution = solver.solve(problem);
// 打印和输出solution
DispatchSolutionUtils.printSolution(solution);
// DispatchSolutionUtils.explainSolutionConstraintDetail(solution);
// DispatchSolutionUtils.exportSolutionJson(solution, "dispatchSolution.json");
// DispatchSolutionUtils.exportMapHtml(solution, "dispatchMap" +
// System.currentTimeMillis());
// FIXME
DispatchSolution movedSolution = DispatchSolutionUtils.moveCustomer(solverFactory, solution,
solution.getTechnicianList().get(0), 0, 1);
DispatchSolutionUtils.printSolution(movedSolution);
}
}
......@@ -26,7 +26,7 @@ spring:
# - optional:nacos:datasource-config.yaml
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.10.0.116:3306/saas_aftersale_test?serverTimezone=Asia/Shanghai
url: jdbc:mysql://10.10.0.54:3306/saas_aftersale_test?serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!