一个用开源google orTools实现的有时间窗、容量限制、可设置接送对(pickup and delivery pair)、支持多路径和灵活起始地点的路径优化问题(CVRPTW: constraint vehicle routing problem with time windows)。程序使用开源求解器和建模工具,支持excel或txt表格形式的标准化输入和输出。
Google optimization tools不仅用C++语言开发了自己的求解器,而且可以通过接口调用几个常用的商用或开源求解器。项目使用了orTools的python接口,虽然功能上只实现了C++的部分功能,但是仍然可以满足项目要求。
对于CVRP问题来说,orTools的优点在于求解器以外又多了一层封装,使开发人员可以更方便地传入对应的参数,省略了很多约束建模的步骤。而且google求解器的工作方式是通过callback函数,相对于pyomo的AML方式,在描述非线性问题建模时更方便。
业务每天有n辆车(n位员工)分别从k个集散中心出发,一天内共需要拜访m个客户站点。在一天结束时所有车辆/员工必须回到出发地。
每辆车有固定费用,以及最大容量、最多客户站点数、最大里程数、最高行驶速度的限制。客户有开放时间窗,对客户的拜访必须在允许的时间窗内完成。在各个客户站点有定义不同的工作时长。
每辆车/每位员工和每个集散中心都有工作开始和结束时间限制。对于车辆/员工来说,从集散中心出发和返回的时间必须在工作时间以内;对于集散中心来说,所有车辆的触发和返回必须在集散中心的开放时间以内。
目标是在满足所有约束的情况下,为每辆车安排一天的行程,使当天能够拜访的客户站点数达到最大值,所有车辆的成本总和最小。
以TSP(travelling salesman problem)为基础,泛化而来的问题是车辆路线规划问题(VRP)。一种常见的VRP问题增加了车辆容量和站点访问时间窗口,是一种特殊的VRP问题,缩写为CVRPTW(constraint vehicle routing problem with time windows)。
TSP的混合整数线性优化描述:
假设集合V有n个站点 v 0 v_0 v0至 v n v_n vn。如果有起始地点depot的集合 V d = v 0 , v 1 , … , v d V_d = {v_0, v_1, \dots, v_d} Vd=v0,v1,…,vd,则
V ’ = V \ { V d } \mathbf V’ = \mathbf V \backslash \big\{ V_{d} \big\} V’=V\{Vd} 代表客户站点。
站点间的连接为
x i j = { 1 站点i和j之间有连接 0 无连接 x_{ij} = \begin{cases} 1 & \quad \text{站点i和j之间有连接} \\ 0 & \quad \text{无连接} \end{cases} xij={10站点i和j之间有连接无连接。
目标函数为:
m i n i m i z e ∑ i = 1 n ∑ j ≠ i , j = 1 n c i j x i j , i , j ∈ V ( 1 ) minimize \sum_{i=1}^n \sum_{j \neq i,j=1}^n c_{ij}x_{ij}, \quad i,j \in \mathbf V \quad (1) minimizei=1∑nj̸=i,j=1∑ncijxij,i,j∈V(1)
c i j c_{ij} cij 是节点间的距离,depot的集合为空。
约束为:
0 ≤ x i j ≤ 1 ( 2 ) 0 \leq x_{ij} \leq 1 \quad (2) 0≤xij≤1(2)
c i j = c j i , i , j ∈ V ( 3 ) c_{ij} = c_{ji}, \quad i, j \in \mathbf V \quad (3) cij=cji,i,j∈V(3),所有路径上的cost都是对称的,
∑ i = 1 , i ≠ j n x i j = 1 , j ∈ V ( 4 ) \sum_{i=1,i \neq j}^n x_{ij} =1, \quad j \in \mathbf{V} \quad (4) i=1,i̸=j∑nxij=1,j∈V(4),从任意节点出发,只连接其他节点中的一个
∑ j = 1 , j ≠ i n x i j = 1 , i ∈ V ( 5 ) \sum_{j=1, j \neq i}^n x_{ij}=1,\quad i \in \mathbf{V} \quad (5) j=1,j̸=i∑nxij=1,i∈V(5),到任意节点的路线,只从其他节点中的一个出发
u i ∈ Z , i ∈ V ( 6 ) u_i \in \mathbf Z,\quad i \in \mathbf{V} \quad (6) ui∈Z,i∈V(6)
u i − u j + n x i j ≤ n − 1 ( 7 ) u_i - u_j + nx_{ij} \leq n-1 \quad (7) ui−uj+nxij≤n−1(7)
0 ≤ u i , u j ≤ n − 1 , 2 ≤ i ≠ j ≤ n ( 8 ) 0 \leq u_i, u_j \leq n-1, \quad 2 \leq i \neq j \leq n \quad (8) 0≤ui,uj≤n−1,2≤i̸=j≤n(8),保证经过所有节点的线路只有一条,而不是分开的很多条。具体见维基百科
CVRP的描述:
(1)、(2)、(3)、(4)、(5) 不变,增加约束:
∑ i n x i 0 = K ( 9 ) \sum_{i}^n x_{i0} = K \quad (9) i∑nxi0=K(9)
∑ j n x 0 j = K ( 10 ) \sum_{j}^n x_{0j} = K \quad (10) j∑nx0j=K(10),离开和进入集散中心(depot)的车辆数相等,且等于车辆总数K。
设每辆车的路线为 R k = { v 0 , v 1 , … , v m + 1 } , v i ∈ V , v 0 = v m + 1 R_k = \big\{ v_0, v_1, \dots, v_{m+1} \big\}, \quad v_i \in \mathbf V, v_0 = v_{m+1} Rk={v0,v1,…,vm+1},vi∈V,v0=vm+1 且 v 0 , v m + 1 ∈ V d , k ∈ K v_0, v_{m+1} \in \mathbf V_d, k \in K v0,vm+1∈Vd,k∈K。
另外设每辆车分别有:
设每个客户站点的需求量为 q i q_i qi,所需服务时间为 δ i \delta_i δi。
时间约束(time cost) T C ( R k ) = ∑ i = 0 m t c v i , v i + 1 + ∑ i = 1 m δ v i ≤ t i m e C o n s t r a i n t k , k ∈ K ( 11 ) TC(R_k) = \sum_{i=0}^m tc_{v_i,v_{i+1}} + \sum_{i=1}^m \delta_{v_i} \leq timeConstraint_k, \quad k \in K \quad (11) TC(Rk)=i=0∑mtcvi,vi+1+i=1∑mδvi≤timeConstraintk,k∈K(11)
其中 t c i j = c i j s p e e d k , i , j ∈ V , k ∈ K tc_{ij} = \cfrac{c_{ij}}{speed_k} , i, j \in \mathbf V, k \in K tcij=speedkcij,i,j∈V,k∈K
距离约束(distance cost) D C ( R k ) = ∑ i = 0 m c v i , v i + 1 ≤ d i s t a n c e C o n s t r a i n t k , ( 12 ) DC(R_k) = \sum_{i=0}^m c_{v_i, v_{i+1}} \leq distanceConstraint_k, \quad (12) DC(Rk)=i=0∑mcvi,vi+1≤distanceConstraintk,(12)
容量约束(capacity cost) C C ( R k ) = ∑ i = 1 m q v i ≤ c a p a c i t y C o n s t r a i n t k , ( 13 ) CC(R_k) = \sum_{i=1}^m q_{v_i} \leq capacityConstraint_k, \quad (13) CC(Rk)=i=1∑mqvi≤capacityConstraintk,(13)
站点数约束(number of site cost) N S C ( R k ) = ∣ R k ∣ − 2 ≤ n r S i t e C o n s t r a i n t k , ( 14 ) NSC(R_k) = |R_k| - 2 \leq nrSiteConstraint_k, \quad (14) NSC(Rk)=∣Rk∣−2≤nrSiteConstraintk,(14)
另外考虑计算效率,设K的下限为
b ( V ) = m a x i m u m ( L B c , L B s ) b(V) = maximum(LB_{c}, LB_{s}) b(V)=maximum(LBc,LBs)
其中
L B c = ∑ i ∈ V ′ q i m a x k ( c a p a c i t y C o n s t r a i n t s k ) LB_c = \cfrac{\sum_{i \in \mathbf V'} q_{i}}{max_k(capacityConstraints_k)} LBc=maxk(capacityConstraintsk)∑i∈V′qi
L B s = ∣ V ′ ∣ m a x k ( n r S i t e C o n s t r a i n t s k ) LB_s = \cfrac{|\mathbf V'|}{max_k(nrSiteConstraints_k)} LBs=maxk(nrSiteConstraintsk)∣V′∣
CVRPTW的描述:
如果考虑客户站点的时间窗口(TwStart, TwEnd):
T w S t a r t i + δ i ≤ T w E n d i , i ∈ V ′ ( 15 ) TwStart_i + \delta_i \leq TwEnd_i, \quad i \in \mathbf V' \quad (15) TwStarti+δi≤TwEndi,i∈V′(15)
到达站点 v i v_i vi 的时间
A r v k , i = ∑ j = 0 i − 1 t c v j , v j + 1 + ∑ j = 1 i − 1 δ v j ≥ T w S t a r t v i , v i , v j ∈ R k ( 16 ) Arv_{k, i} = \sum_{j=0}^{i-1} tc_{v_j, v_{j+1}} + \sum_{j=1}^{i-1} \delta_{v_j} \geq TwStart_{v_i}, \quad v_i, v_j \in R_k \quad (16) Arvk,i=j=0∑i−1tcvj,vj+1+j=1∑i−1δvj≥TwStartvi,vi,vj∈Rk(16)
离开站点 v i v_i vi 的时间
D e p k , i = A r v k , i + δ v i ≤ T w E n d v i , v i ∈ R k ( 17 ) Dep_{k, i} = Arv_{k, i} + \delta_{v_i} \leq TwEnd{v_i}, \quad v_i \in R_k \quad (17) Depk,i=Arvk,i+δvi≤TwEndvi,vi∈Rk(17)
orTools里cost的计算是通过callback函数实现的,函数的格式是function(from_node_index, to_node_index)
。在生成约束的时候把函数直接作为变量传递给orTools的类。
计算任意两个节点间距离的函数:
class CreateDistanceEvaluator():
def __init__(self, locationData, multiplier=1.3):
self.distances = {}
self._multiplier = multiplier
for from_node in np.arange(len(locationData)):
self.distances[from_node] = {}
for to_node in np.arange(len(locationData)):
if np.alltrue(np.equal(from_node, to_node)):
self.distances[from_node][to_node] = 0
else:
self.distances[from_node][to_node] = convertToInt(
self.getDistanceInKm(locationData[from_node],
locationData[to_node]) * self._multiplier)
@staticmethod
def getDistanceInKm(coord1,coord2):
# https://en.wikipedia.org/wiki/Haversine_formula
lat1, lon1 = coord1
lat2, lon2 = coord2
if np.isnan(lat1 * lon1 * lat2 * lon2):
return 0
def deg2rad(deg):
return deg * (math.pi / 180)
R = 6371 # 地球半径(公里)
dLat = deg2rad(lat2-lat1)
dLon = deg2rad(lon2-lon1);
a = ( math.sin(dLat/2) * math.sin(dLat/2)
+ math.cos(deg2rad(lat1)) * math.cos(deg2rad(lat2)) *
math.sin(dLon/2) * math.sin(dLon/2) )
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
d = R * c
return d
def distance_evaluator(self, from_node, to_node):
"""
callback函数。在计算cost的过程中直接抽取预先计算的距离值,提高速度。
"""
return self.distances[from_node][to_node]
计算节点需求的callback函数,也用来计算访问节点数:
class CreateDemandEvaluator():
def __init__(self, demandData):
self._demands = demandData
def demand_evaluator(self, from_node, to_node):
"""
Callback函数
"""
del to_node
return self._demands[from_node]
计算时间cost的callback函数。
由于每辆车的速度不同,计算cost的方式有区别,需要生成一组不同的evaluator类。
class CreateAllTransitEvaluators():
def __init__(self, vehicles, distances, serviceTimes):
"""
callback函数list
"""
self._vehicles = vehicles
self._distances = distances
self._serviceTimes = serviceTimes
self.evaluators = []
# 每辆车根据速度不同单独生成对应的evaluator:
for v in vehicles.speeds:
evaluator = CreateOneTransitEvaluator(v, self._distances,
self._serviceTimes).one_transit_evaluator
self.evaluators.append(evaluator)
class CreateOneTransitEvaluator():
def __init__(self, speed, distances, serviceTimes):
self._speed = speed
self._distances = distances
self._serviceTimes = serviceTimes
def one_transit_evaluator(self, from_node, to_node):
"""
单一callback函数:
计算单个节点的时间总量 = 当前节点到下一节点的距离 / 车辆速度 + 当前节点的服务时长
"""
if from_node == to_node:
return 0
if self._speed == 0:
return sys.maxsize
return convertToInt(self._distances[from_node][to_node] / self._speed
+ self._serviceTimes[from_node])
定义约束:RoutingModel类的接口定义
其中AddDimension(evaluator, slack, capacity, *args)
用来定义约束,常用有4种:
另外:
fix_start_cumul_to_zero
定义了cost是否默认从0开始计数。当涉及时间窗口,time cost的起始点大于0,这一项需要设置为false.def add_capacity_constraints(routing, listOfConstraints, evaluator, varName):
name = varName
routing.AddDimensionWithVehicleCapacity(
evaluator,
0,
listOfConstraints,
True,
name)
def add_transit_and_capacity_constraints(routing, listOfConstraints,
listOfEvaluators, intSlack, varName):
name = varName
routing.AddDimensionWithVehicleTransitAndCapacity(
listOfEvaluators,
intSlack,
listOfConstraints,
False,
name)
定义时间窗口约束:
RoutingModel.GetDimensionOrDie
函数根据给定的约束维度名称提取对应的约束信息。
从dimension里返回的值有几类:
时间窗口的约束就是加在每个cumulative 变量上的,所以需要首先调用RoutingModel.GetDimensionOrDie
获取CumulVar的值。
另外注意几点细节:
AddToAssignment(SlackVar)
。SlackVar(RoutingModel.End(vehicle_id)
,程序会报错。def add_timewindow_constraints(routing, data, varName='time'):
time_dimension = routing.GetDimensionOrDie(varName)
for node_idx, time_window in enumerate(data.timeWindows):
if node_idx <= np.max(data.depotIndex):
continue
index = routing.NodeToIndex(node_idx)
servTime = data.serviceTimes[node_idx]
time_dimension.CumulVar(index).SetRange(
time_window[0], time_window[1]-servTime)
routing.AddToAssignment(time_dimension.SlackVar(index))
for veh_idx in np.arange(data.nrVehicles):
index = routing.Start(veh_idx)
servTime = data.serviceTimes[data.depotIndex[veh_idx]]
time_dimension.CumulVar(index).SetRange(
data.earliestWorkHours[veh_idx],
data.latestWorkHours[veh_idx]-servTime)
routing.AddToAssignment(time_dimension.SlackVar(index))
for veh_idx in np.arange(len(data.depotIndex)):
index = routing.End(veh_idx)
servTime = data.serviceTimes[data.depotIndex[veh_idx]]
time_dimension.CumulVar(index).SetRange(
data.earliestWorkHours[veh_idx],
data.latestWorkHours[veh_idx]-servTime)
用ConsolePrinter类打印结果:主要通过调用GetDimensionOrDie函数获取各个约束维度的信息:
class ConsolePrinter():
def __init__(self, data, routing, assignment, distances):
self._data = data
self._routing = routing
self._assignment = assignment
self._distances = distances
def printAll(self):
total_dist = 0
total_siteCount = 0
total_fulfilledDemand = 0
capacity_dimension = self._routing.GetDimensionOrDie('capacity')
distance_dimension = self._routing.GetDimensionOrDie('dailyDistance')
time_dimension = self._routing.GetDimensionOrDie('time')
siteCount_dimension = self._routing.GetDimensionOrDie('dailyNrJobs')
for vehicle_id in np.arange(self._data.nrVehicles):
index = self._routing.Start(vehicle_id)
plan_output = 'Route for person {0}: \n'.format(vehicle_id)
route_startTime = self._assignment.Value(time_dimension.CumulVar(index))
route_serviceTime = 0
route_timeWindow = []
while not self._routing.IsEnd(index):
node_index = self._routing.IndexToNode(index)
next_node_index = self._routing.IndexToNode(
self._assignment.Value(self._routing.NextVar(index)))
step_dist = self._distances[node_index][next_node_index]
step_load = self._data.demands[node_index]
step_serviceTime = self._data.serviceTimes[node_index]
route_serviceTime += step_serviceTime
step_timewindow = self._data.timeWindows[node_index]
route_timeWindow.append(step_timewindow)
time_var = time_dimension.CumulVar(index)
time_min = self._assignment.Min(time_var)
time_max = self._assignment.Max(time_var)
slack_var = time_dimension.SlackVar(index)
slack_min = self._assignment.Min(slack_var)
slack_max = self._assignment.Max(slack_var)
plan_output += (
' {node} capacity({capa}) distance({dist}) serviceTime({minTime},{maxTime}) slack({minSlack},{maxSlack})->\n'
.format(node=node_index, capa=step_load, dist=step_dist,
minTime=time_min, maxTime=time_max, minSlack=slack_min,
maxSlack=slack_max) )
index = self._assignment.Value(self._routing.NextVar(index))
end_idx = self._routing.End(vehicle_id)
route_endTime = self._assignment.Value(time_dimension.CumulVar(end_idx))
route_dist = self._assignment.Value(distance_dimension.CumulVar(end_idx))
route_load = self._assignment.Value(capacity_dimension.CumulVar(end_idx))
route_siteCount = self._assignment.Value(siteCount_dimension.CumulVar(end_idx))
node_index = self._routing.IndexToNode(index)
total_dist += route_dist
total_siteCount += route_siteCount
total_fulfilledDemand += route_load
plan_output += ' {0} \n'.format(node_index)
plan_output += ('Objective: minimize vehicle cost + distance cost, maximize number of sites visited\nConstraint:\n 1.vehicle capacity {load} pieces\n 2.vehicle daily distance {dailyDistance} km\n 3.vehicle daily sites {dailySites}\n 4.depot opening hours {depotTime} min\n 5.vehicle shift times {vehicleTime} min\n 6.location time windows {tw}\n'
.format(load=self._data.vehicles.capacity[vehicle_id],
depotTime=self._data.timeWindows[self._data.depotIndex[vehicle_id]],
tw=route_timeWindow,
dailyDistance = self._data.vehicles.dailyDistanceLimit[vehicle_id],
dailySites = self._data.vehicles.nrJobLimit[vehicle_id],
vehicleTime = self._data.vehicles.vehicleTimeWindows[vehicle_id]
) )
plan_output += 'Result:\n 1.load of the route: {0} pcs\n'.format(route_load)
plan_output += ' 2.distance of the route: {0} km\n'.format(route_dist)
plan_output += ' 3.visited nr. sits: {0}\n'.format(route_siteCount)
plan_output += ' 4.timespan of the route: ({0},{1}) min\n'.format(
route_startTime, route_endTime)
plan_output += ' of which service time: {0} min\n'.format(route_serviceTime)
print(plan_output)
print('Total distance of all routes: {0} km\nTotal nr. visited sites: {1}\nTotal fulfilled demand: {2}\n'
.format(total_dist,total_siteCount,total_fulfilledDemand))
print('Dropped nodes: {0}\n').format(self.getDropped())
def getDropped(self):
dropped = []
for idx in np.arange(self._routing.Size()):
if self._assignment.Value(self._routing.NextVar(idx)) == idx:
dropped.append(idx)
return dropped
在console里画出每条路线:这几个函数主要参考了google的案例代码:
def discrete_cmap(N, base_cmap=None):
"""
based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
Create an N-bin discrete colormap from the specified input map
"""
# Note that if base_cmap is a string or None, you can simply do
# return plt.cm.get_cmap(base_cmap, N)
# The following works for string, None, or a colormap instance:
base = plt.cm.get_cmap(base_cmap)
color_list = base(np.linspace(0, 1, N))
cmap_name = base.name + str(N)
return base.from_list(cmap_name, color_list, N)
def build_vehicle_route(routing, assignment, locations, vehicle_id):
"""
based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
Build a route for a vehicle by starting at the strat node and
continuing to the end node.
"""
veh_used = routing.IsVehicleUsed(assignment, vehicle_id)
if veh_used:
route = []
node = routing.Start(vehicle_id) # Get the starting node index
route.append(locations[routing.IndexToNode(node)])
while not routing.IsEnd(node):
route.append(locations[routing.IndexToNode(node)])
node = assignment.Value(routing.NextVar(node))
route.append(locations[routing.IndexToNode(node)])
return route
else:
return None
def plot_vehicle_routes(vehicle_routes, ax, data):
"""
based on https://github.com/google/or-tools/blob/master/examples/python/cvrptw_plot.py
Plot the vehicle routes on matplotlib axis ax1.
"""
veh_used = [v for v in vehicle_routes if vehicle_routes[v] is not None]
cmap = discrete_cmap(len(data.vehicles.names) + 2, 'nipy_spectral')
for veh_number in veh_used:
lats, lons = zip(*[(c[0], c[1]) for c in vehicle_routes[veh_number]])
lats = np.array(lats)
lons = np.array(lons)
ax.plot(lons, lats, 'o', mfc=cmap(veh_number + 1))
ax.quiver(
lons[:-1],
lats[:-1],
lons[1:] - lons[:-1],
lats[1:] - lats[:-1],
scale_units='xy',
angles='xy',
scale=1,
color=cmap(veh_number + 1))
在main函数中完成约束建模。运行结果见下:
# 使用默认的建模参数:
model_parameters = pywrapcp.RoutingModel.DefaultModelParameters()
routing = pywrapcp.RoutingModel(
data.nrLocations,
data.nrVehicles,
data.depotIndex,
data.depotIndex,
model_parameters)
# 添加车辆成本:
for n,v in enumerate(data.vehicles.costs):
routing.SetFixedCostOfVehicle(v, n)
# 添加距离成本:
distEval = CreateDistanceEvaluator(data.locations)
distance_evaluator = distEval.distance_evaluator # callback函数
routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator)
# 添加每辆车的最大行驶距离约束:
add_capacity_constraints(routing, data.vehicles.dailyDistanceLimit,
distance_evaluator, 'dailyDistance')
# 添加每辆车的最大容量约束:
demand_evaluator = CreateDemandEvaluator(data.demands).demand_evaluator
add_capacity_constraints(routing, data.vehicles.capacity,
demand_evaluator, 'capacity')
# 添加每辆车的最多访问站点数约束:
nrJobs_evaluator = CreateDemandEvaluator(
data.visitedLocations).demand_evaluator
add_capacity_constraints(routing, data.vehicles.nrJobLimit,
nrJobs_evaluator, 'dailyNrJobs')
# 添加运营时间约束
transitEval = CreateAllTransitEvaluators(data.vehicles, distEval.distances,
data.serviceTimes)
add_transit_and_capacity_constraints(routing, data.latestWorkHours,
transitEval.evaluators,
int(np.max(data.latestWorkHours)), 'time')
# 添加时间窗口约束
add_timewindow_constraints(routing, data, 'time')
# 设置搜索策略(本例中主要使用了默认参数,其他参数参考google教程):
# https://developers.google.com/optimization/routing/routing_options
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
# 设置惩罚项,允许有客户站点不被访问(当所有车辆达到约束上限时)
# 否则问题有可能无解。
non_depot = set(range(data.nrLocations))
non_depot.difference_update(data.depotIndex)
penalty = 400000
nodes = [routing.AddDisjunction([c], penalty) for c in non_depot]
# 求解
assignment = routing.SolveWithParameters(search_parameters)
# 打印结果
printer = ConsolePrinter(data, routing, assignment, distEval.distances)
printer.printAll()
# plot routes
dropped = []
dropped = printer.getDropped()
vehicle_routes = {}
for veh in np.arange(data.nrVehicles):
vehicle_routes[veh] = build_vehicle_route(routing, assignment,
data.locations, veh)
# Plotting of the routes in matplotlib.
fig = plt.figure()
ax = fig.add_subplot(111)
# Plot all the nodes as black dots.
clon, clat = zip(*[(c[0], c[1]) for i,c in enumerate(data.locations) if i not in dropped])
ax.plot(clat, clon, 'k.')
# plot the routes as arrows
plot_vehicle_routes(vehicle_routes, ax, data)
高德地图提供API给前端开发者上传位置信息,可以在实际的地图中显示规划完成的路线。具体操作可以参考:https://cn.aliyun.com/jiaocheng/441387.html 。