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