1 /**********************************************************************************************
2 Copyright (C) 2020 Henri Hornburg <hrnbg@t-online.de>
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 **********************************************************************************************/
18
19 #include "CRouterOptimization.h"
20 #include "gis/GeoMath.h"
21 #include "gis/rte/router/CRouterSetup.h"
22 #include "helpers/CProgressDialog.h"
23
CRouterOptimization()24 CRouterOptimization::CRouterOptimization()
25 {
26 routerOptions = CRouterSetup::self().getOptions();
27 }
28
optimize(SGisLine & line)29 int CRouterOptimization::optimize(SGisLine& line)
30 {
31 checkRouter();
32 if(!CRouterSetup::self().hasFastRouting())
33 {
34 return -1;
35 }
36 if(line.length() < 4)
37 {
38 return 0; //There is nothing to optimize
39 }
40
41 CProgressDialog progress(tr("Optimizing route"), 0, line.length() + 2, nullptr);
42
43 //Optimize using air distance and known distances, since this is much faster than routing, especially brouter
44 SGisLine newAirdistanceOrder;
45 SGisLine oldAirdistanceOrder = line;
46 qreal gain = createNextBestOrder(oldAirdistanceOrder, newAirdistanceOrder);
47 while(gain < 0)
48 {
49 oldAirdistanceOrder = newAirdistanceOrder;
50 gain = createNextBestOrder(oldAirdistanceOrder, newAirdistanceOrder);
51 }
52 //Do routing and calculate cost for order found
53 qreal airdistanceOrderCosts = getRealRouteCosts(oldAirdistanceOrder);
54
55 progress.setValue(1);
56
57 //Do routing and calculate costs of the order the user supplied
58 //cancel the route calculation when the cost of the airdistance order is reached
59 qreal givenOrderCosts = getRealRouteCosts(line, airdistanceOrderCosts);
60 if(givenOrderCosts < 0 && airdistanceOrderCosts < 0)
61 {
62 return -1;
63 }
64
65 //determine starting order for optimization
66 qreal bestCosts = NOINT;
67 if(givenOrderCosts > 0 && givenOrderCosts < airdistanceOrderCosts)
68 {
69 bestCosts = givenOrderCosts;
70 }
71 else
72 {
73 bestCosts = airdistanceOrderCosts;
74 //the old order is the "optimal" one, since the new one didn't have a gain<0
75 line = oldAirdistanceOrder;
76 fillSubPts(line);
77 }
78 progress.setValue(2);
79
80 SGisLine lastWorkingOrder = line;
81 qreal lastWorkingOrderCosts = bestCosts;
82 int numOfRestarts = 0;
83 // The number of needed starting permutations is somewhat arbitrary,
84 // but you'd likely need more to find the global optimum if there are more possibilities
85 while(numOfRestarts < line.length())
86 {
87 progress.setValue(numOfRestarts + 2);
88 if(progress.wasCanceled())
89 {
90 return -1;
91 }
92
93 SGisLine newWorkingOrder;
94 qreal bestInsertionGain = createNextBestOrder(lastWorkingOrder, newWorkingOrder);
95
96 if(bestInsertionGain < 0)
97 {
98 qreal newWorkingOrderCosts = getRealRouteCosts(newWorkingOrder, lastWorkingOrderCosts);
99
100 if(newWorkingOrderCosts > 0 && newWorkingOrderCosts < lastWorkingOrderCosts)
101 {
102 lastWorkingOrder = newWorkingOrder;
103 lastWorkingOrderCosts = newWorkingOrderCosts;
104
105 if(newWorkingOrderCosts < bestCosts)
106 {
107 bestCosts = newWorkingOrderCosts;
108 line = newWorkingOrder;
109
110 // Do this every time, since changes to the line are displayed and the data is available anyways.
111 // Gives a nice animation on screen if the subpoints are displayed aswell.
112 fillSubPts(line);
113 }
114 }
115 }
116 else
117 {
118 numOfRestarts += 1;
119 //We accept any order that is produced, as we want to escape from the local optimum
120 twoOptStep(lastWorkingOrder, newWorkingOrder);
121 lastWorkingOrder = newWorkingOrder;
122 lastWorkingOrderCosts = getRealRouteCosts(lastWorkingOrder);
123 }
124 }
125
126 //Do this a last time, since it happens that the route is already optimal.
127 //Return the return value as this is the last point the code may fail for some odd reason
128 return fillSubPts(line);
129 }
130
createNextBestOrder(const SGisLine & oldOrder,SGisLine & newOrder)131 qreal CRouterOptimization::createNextBestOrder(const SGisLine& oldOrder, SGisLine& newOrder)
132 {
133 qreal bestInsertionGain = 0;
134 int bestBaseIndex = -1;
135 int bestInsertedItemIndex = -1;
136
137 // lastWorkingOrder.length()-2, since we can't use the last two items as base
138 for(int baseIndex = 0; baseIndex < oldOrder.length() - 2; baseIndex++)
139 {
140 // Keep start and end fixed
141 for(int insertedItemIndex = 1; insertedItemIndex < oldOrder.length() - 1; insertedItemIndex++)
142 {
143 if(baseIndex == insertedItemIndex || baseIndex == insertedItemIndex - 1)
144 {
145 continue;
146 }
147
148 qreal insertionGain = bestKnownDistance(oldOrder[baseIndex], oldOrder[insertedItemIndex])
149 + bestKnownDistance(oldOrder[insertedItemIndex], oldOrder[baseIndex + 1])
150 - bestKnownDistance(oldOrder[baseIndex], oldOrder[baseIndex + 1])
151 + bestKnownDistance(oldOrder[insertedItemIndex - 1], oldOrder[insertedItemIndex + 1])
152 - bestKnownDistance(oldOrder[insertedItemIndex - 1], oldOrder[insertedItemIndex])
153 - bestKnownDistance(oldOrder[insertedItemIndex], oldOrder[insertedItemIndex + 1]);
154
155 if(insertionGain < bestInsertionGain)
156 {
157 bestBaseIndex = baseIndex;
158 bestInsertionGain = insertionGain;
159 bestInsertedItemIndex = insertedItemIndex;
160 }
161 }
162 }
163
164 newOrder = SGisLine(oldOrder);
165 if(bestBaseIndex >= 0 && bestInsertedItemIndex >= 0)
166 {
167 // If the index of the inserted item was smaller than that of the base item,
168 // moving it will cause the index of the base to decrease. Thus, we don't add 1 to place it after the base
169 if(bestInsertedItemIndex < bestBaseIndex)
170 {
171 newOrder.move(bestInsertedItemIndex, bestBaseIndex);
172 }
173 else
174 {
175 newOrder.move(bestInsertedItemIndex, bestBaseIndex + 1);
176 }
177 }
178 return bestInsertionGain;
179 }
180
twoOptStep(const SGisLine & oldOrder,SGisLine & newOrder)181 qreal CRouterOptimization::twoOptStep(const SGisLine& oldOrder, SGisLine& newOrder)
182 {
183 //NOINT and not 0, since we also want to take orders that don't seem to be improving the situation
184 qreal bestTwoOptGain = NOINT;
185 //Begin and End of the section that is inverted
186 int bestBeginIndex = -1;
187 int bestEndIndex = -1;
188
189 // lastWorkingOrder.length()-2, since the end of the inverted section can't be the end of the line
190 for(int beginIndex = 1; beginIndex < oldOrder.length() - 2; beginIndex++)
191 {
192 for(int endIndex = beginIndex + 1; endIndex < oldOrder.length() - 1; endIndex++)
193 {
194 qreal oldRangeCosts = bestKnownDistance(oldOrder[beginIndex - 1], oldOrder[beginIndex])
195 + bestKnownDistance(oldOrder[endIndex], oldOrder[endIndex + 1]);
196
197 qreal newRangeCosts = bestKnownDistance(oldOrder[beginIndex - 1], oldOrder[endIndex])
198 + bestKnownDistance(oldOrder[beginIndex], oldOrder[endIndex + 1]);
199 for(int i = beginIndex; i < endIndex; i++)
200 {
201 oldRangeCosts += bestKnownDistance(oldOrder[i], oldOrder[i + 1]);
202 newRangeCosts += bestKnownDistance(oldOrder[i + 1], oldOrder[i]);
203 }
204
205 if(newRangeCosts - oldRangeCosts < bestTwoOptGain)
206 {
207 bestBeginIndex = beginIndex;
208 bestEndIndex = endIndex;
209 bestTwoOptGain = newRangeCosts - oldRangeCosts;
210 }
211 }
212 }
213
214 newOrder = SGisLine(oldOrder);
215 if(bestEndIndex >= 0 && bestBeginIndex >= 0)
216 {
217 std::reverse(newOrder.begin() + bestBeginIndex, newOrder.begin() + bestEndIndex);
218 }
219 return bestTwoOptGain;
220 }
221
getRealRouteCosts(const SGisLine & line,qreal costCutoff)222 qreal CRouterOptimization::getRealRouteCosts(const SGisLine& line, qreal costCutoff)
223 {
224 qreal costs = 0;
225 for(int i = 0; i < line.length() - 1; i++)
226 {
227 const routing_cache_item_t* route = getRoute(line[i].coord, line[i + 1].coord);
228 if(route == nullptr)
229 {
230 return -1;
231 }
232 costs += route->costs;
233
234 if(costCutoff > 0 && costs > costCutoff)
235 {
236 return -1;
237 }
238 }
239 return costs;
240 }
241
bestKnownDistance(const IGisLine::point_t & start,const IGisLine::point_t & end)242 qreal CRouterOptimization::bestKnownDistance(const IGisLine::point_t& start, const IGisLine::point_t& end)
243 {
244 //10 digits after the decimal point in exponential format should be by far enough
245 QString start_key = QString::number(start.coord.x(), 'e', 10) + QString::number(start.coord.y(), 'e', 10);
246 QString end_key = QString::number(end.coord.x(), 'e', 10) + QString::number(end.coord.y(), 'e', 10);
247
248 if(!routingCache.contains(start_key))
249 {
250 routingCache[start_key] = {};
251 }
252
253 if(routingCache[start_key].contains(end_key))
254 {
255 return routingCache[start_key][end_key].costs;
256 }
257 else
258 {
259 //Multiply it with the average of the minimum occuring factor and the average factor
260 // to get a reasonable compromise of optimization speed and optimality of results
261 if(totalNumOfRoutes > 0 && minAirToCostFactor > 0)
262 {
263 return GPS_Math_DistanceQuick(start.coord.x(), start.coord.y(),
264 end.coord.x(), end.coord.y())
265 * (totalAirToCosts / totalNumOfRoutes + minAirToCostFactor) / 2;
266 }
267 else
268 {
269 return GPS_Math_DistanceQuick(start.coord.x(), start.coord.y(),
270 end.coord.x(), end.coord.y());
271 }
272 }
273 }
274
getRoute(const QPointF & start,const QPointF & end)275 const CRouterOptimization::routing_cache_item_t* CRouterOptimization::getRoute(const QPointF& start, const QPointF& end)
276 {
277 QString start_key = QString::number(start.x(), 'e', 10) + QString::number(start.y(), 'e', 10);
278 QString end_key = QString::number(end.x(), 'e', 10) + QString::number(end.y(), 'e', 10);
279
280 if(!routingCache.contains(start_key))
281 {
282 routingCache[start_key] = {};
283 }
284
285 if(!routingCache[start_key].contains(end_key))
286 {
287 routing_cache_item_t cacheItem;
288 int response = CRouterSetup::self().calcRoute(start, end, cacheItem.route, &cacheItem.costs);
289 if(response < 0)
290 {
291 return nullptr;
292 }
293 routingCache[start_key][end_key] = cacheItem;
294
295 qreal airToCostFactor = cacheItem.costs / GPS_Math_DistanceQuick(start.x(), start.y(), end.x(), end.y());
296 if( airToCostFactor < minAirToCostFactor || minAirToCostFactor < 0)
297 {
298 minAirToCostFactor = airToCostFactor;
299 }
300 totalAirToCosts += airToCostFactor;
301 totalNumOfRoutes++;
302 }
303 return &routingCache[start_key][end_key];
304 }
305
fillSubPts(SGisLine & line)306 int CRouterOptimization::fillSubPts(SGisLine& line)
307 {
308 for(int i = 0; i < line.length() - 1; i++)
309 {
310 line[i].subpts.clear();
311 const routing_cache_item_t* route = getRoute(line[i].coord, line[i + 1].coord);
312 if(route == nullptr)
313 {
314 return -1;
315 }
316 for(const QPointF& point : route->route)
317 {
318 line[i].subpts << IGisLine::subpt_t(point);
319 }
320 }
321 return 0;
322 }
323
checkRouter()324 void CRouterOptimization::checkRouter()
325 {
326 const QString& options = CRouterSetup::self().getOptions();
327 if(routerOptions != options)
328 {
329 routingCache.clear();
330 routerOptions = options;
331 }
332 }
333