1 /**********************************************************************************************
2     Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.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 "canvas/CCanvas.h"
20 #include "CMainWindow.h"
21 #include "gis/CGisWorkspace.h"
22 #include "gis/proj_x.h"
23 #include "gis/rte/CGisItemRte.h"
24 #include "gis/rte/router/CRouterRoutino.h"
25 #include "gis/rte/router/routino/CRouterRoutinoPathSetup.h"
26 #include "helpers/CProgressDialog.h"
27 #include "helpers/CSettings.h"
28 #include "setup/IAppSetup.h"
29 
30 #include <QtWidgets>
31 #include <routino.h>
32 
33 QPointer<CProgressDialog> CRouterRoutino::progress;
34 
ProgressFunc(double complete)35 int ProgressFunc(double complete)
36 {
37     if(CRouterRoutino::progress.isNull())
38     {
39         return true;
40     }
41 
42     CRouterRoutino::progress->setValue(complete * 100);
43 
44     return !CRouterRoutino::progress->wasCanceled();
45 }
46 
47 CRouterRoutino* CRouterRoutino::pSelf = nullptr;
48 
CRouterRoutino(QWidget * parent)49 CRouterRoutino::CRouterRoutino(QWidget* parent)
50     : IRouter(true, parent)
51 {
52     pSelf = this;
53     setupUi(this);
54 
55     connect(labelHelp, &QLabel::linkActivated, &CMainWindow::self(), static_cast<void (CMainWindow::*)(const QString&)>(&CMainWindow::slotLinkActivated));
56 
57     if(Routino_CheckAPIVersion() != ROUTINO_ERROR_NONE)
58     {
59         QMessageBox::warning(this, tr("Warning..."), tr("Found Routino with a wrong version. Expected %1 found %2").arg(ROUTINO_API_VERSION).arg(Routino_APIVersion), QMessageBox::Ok);
60         return;
61     }
62 
63     comboMode->addItem(tr("Shortest"));
64     comboMode->addItem(tr("Quickest"));
65 
66 
67     int res = 0;
68     IAppSetup* setup = IAppSetup::getPlatformInstance();
69     res = Routino_ParseXMLTranslations(setup->routinoPath("translations.xml").toUtf8());
70     if(res)
71     {
72         QMessageBox::critical(this, "Routino...", xlateRoutinoError(Routino_errno), QMessageBox::Abort);
73         return;
74     }
75 
76     comboProfile->addItem(tr("Foot"), "foot");
77     comboProfile->addItem(tr("Horse"), "horse");
78     comboProfile->addItem(tr("Wheelchair"), "wheelchair");
79     comboProfile->addItem(tr("Bicycle"), "bicycle");
80     comboProfile->addItem(tr("Moped"), "moped");
81     comboProfile->addItem(tr("Motorcycle"), "motorcycle");
82     comboProfile->addItem(tr("Motorcar"), "motorcar");
83     comboProfile->addItem(tr("Goods"), "goods");
84 
85     comboLanguage->addItem(tr("English"), "en");
86     comboLanguage->addItem(tr("German"), "de");
87     comboLanguage->addItem(tr("French"), "fr");
88     comboLanguage->addItem(tr("Hungarian"), "hu");
89     comboLanguage->addItem(tr("Dutch"), "nl");
90     comboLanguage->addItem(tr("Russian"), "ru");
91     comboLanguage->addItem(tr("Polish"), "pl");
92     comboLanguage->addItem(tr("Czech"), "cs");
93     comboLanguage->addItem(tr("Spanish"), "es");
94 
95     connect(toolSetupPaths, &QToolButton::clicked, this, &CRouterRoutino::slotSetupPaths);
96 
97     SETTINGS;
98     dbPaths = cfg.value("Route/routino/paths", dbPaths).toStringList();
99     buildDatabaseList();
100 
101     comboProfile->setCurrentIndex(cfg.value("Route/routino/profile", 0).toInt());
102     comboLanguage->setCurrentIndex(cfg.value("Route/routino/language", 0).toInt());
103     comboMode->setCurrentIndex(cfg.value("Route/routino/mode", 0).toInt());
104     comboDatabase->setCurrentIndex(cfg.value("Route/routino/database", 0).toInt());
105 
106     updateHelpText();
107 }
108 
~CRouterRoutino()109 CRouterRoutino::~CRouterRoutino()
110 {
111     SETTINGS;
112     cfg.setValue("Route/routino/paths", dbPaths);
113     cfg.setValue("Route/routino/profile", comboProfile->currentIndex());
114     cfg.setValue("Route/routino/language", comboLanguage->currentIndex());
115     cfg.setValue("Route/routino/mode", comboMode->currentIndex());
116     cfg.setValue("Route/routino/database", comboDatabase->currentIndex());
117 
118     freeDatabaseList();
119     Routino_FreeXMLProfiles();
120     Routino_FreeXMLTranslations();
121 }
122 
xlateRoutinoError(int err)123 QString CRouterRoutino::xlateRoutinoError(int err)
124 {
125     switch(err)
126     {
127     case ROUTINO_ERROR_NO_DATABASE:
128         return tr("A function was called without the database variable set.");
129 
130     case ROUTINO_ERROR_NO_PROFILE:
131         return tr("A function was called without the profile variable set.");
132 
133     case ROUTINO_ERROR_NO_TRANSLATION:
134         return tr("A function was called without the translation variable set.");
135 
136     case ROUTINO_ERROR_NO_DATABASE_FILES:
137         return tr("The specified database to load did not exist.");
138 
139     case ROUTINO_ERROR_BAD_DATABASE_FILES:
140         return tr("The specified database could not be loaded.");
141 
142     case ROUTINO_ERROR_NO_PROFILES_XML:
143         return tr("The specified profiles XML file did not exist.");
144 
145     case ROUTINO_ERROR_BAD_PROFILES_XML:
146         return tr("The specified profiles XML file could not be loaded.");
147 
148     case ROUTINO_ERROR_NO_TRANSLATIONS_XML:
149         return tr("The specified translations XML file did not exist.");
150 
151     case ROUTINO_ERROR_BAD_TRANSLATIONS_XML:
152         return tr("The specified translations XML file could not be loaded.");
153 
154     case ROUTINO_ERROR_NO_SUCH_PROFILE:
155         return tr("The requested profile name does not exist in the loaded XML file.");
156 
157     case ROUTINO_ERROR_NO_SUCH_TRANSLATION:
158         return tr("The requested translation language does not exist in the loaded XML file.");
159 
160     case ROUTINO_ERROR_NO_NEARBY_HIGHWAY:
161         return tr("In the routing database there is no highway near the coordinates to place a waypoint.");
162 
163     case ROUTINO_ERROR_PROFILE_DATABASE_ERR:
164         return tr("The profile and database do not work together.");
165 
166     case ROUTINO_ERROR_NOTVALID_PROFILE:
167         return tr("The profile being used has not been validated.");
168 
169     case ROUTINO_ERROR_BAD_USER_PROFILE:
170         return tr("The user specified profile contained invalid data.");
171 
172     case ROUTINO_ERROR_BAD_OPTIONS:
173         return tr("The routing options specified are not consistent with each other.");
174 
175     case ROUTINO_ERROR_WRONG_API_VERSION:
176         return tr("There is a mismatch between the library and caller API version.");
177 
178     case ROUTINO_ERROR_PROGRESS_ABORTED:
179         return tr("Route calculation was aborted by user.");
180     }
181 
182     if(ROUTINO_ERROR_NO_ROUTE_1 <= err)
183     {
184         int n = err - 1000;
185         return tr("A route could not be found to waypoint %1.").arg(n);
186     }
187 
188     return tr("Unknown error: %1").arg(err);
189 }
190 
hasFastRouting()191 bool CRouterRoutino::hasFastRouting()
192 {
193     return IRouter::hasFastRouting() && (comboDatabase->count() != 0);
194 }
195 
getOptions()196 QString CRouterRoutino::getOptions()
197 {
198     QString str;
199 
200     str = tr("profile \"%1\"").arg(comboProfile->currentText());
201     str += tr(", mode \"%1\"").arg(comboMode->currentText());
202     return str;
203 }
204 
setupPath(const QString & path)205 void CRouterRoutino::setupPath(const QString& path)
206 {
207     if(dbPaths.contains(path))
208     {
209         return;
210     }
211 
212     dbPaths << path;
213     buildDatabaseList();
214     updateHelpText();
215 }
216 
slotSetupPaths()217 void CRouterRoutino::slotSetupPaths()
218 {
219     CRouterRoutinoPathSetup dlg(dbPaths);
220     dlg.exec();
221 
222     buildDatabaseList();
223     updateHelpText();
224 }
225 
buildDatabaseList()226 void CRouterRoutino::buildDatabaseList()
227 {
228     QRegExp re("(.*)-segments.mem");
229     freeDatabaseList();
230 
231     // initialise
232     currentProfilesPath = "";
233 
234     IAppSetup* setup = IAppSetup::getPlatformInstance();
235 
236     for(const QString& path : qAsConst(dbPaths))
237     {
238         QDir dir(path);
239         const QStringList& filenames = dir.entryList(QStringList("*segments.mem"), QDir::Files | QDir::Readable, QDir::Name);
240         for(const QString& filename : filenames)
241         {
242             QString prefix;
243             if(re.exactMatch(filename))
244             {
245                 prefix = re.cap(1);
246             }
247             else
248             {
249                 continue;
250             }
251 
252             // qDebug() << "buildDatabase Prefix" << prefix;
253 
254 #ifdef Q_OS_WIN
255             Routino_Database* data = Routino_LoadDatabase(dir.absolutePath().toLocal8Bit(), prefix.toLocal8Bit());
256 #else
257             Routino_Database* data = Routino_LoadDatabase(dir.absolutePath().toUtf8(), prefix.toUtf8());
258 #endif
259             qDebug() << "Loaded Routino DB" << dir.absolutePath().toUtf8().data() << "  " << prefix.toUtf8().data();
260 
261             if(data == nullptr)
262             {
263                 QMessageBox::critical(this, "Routino ...", xlateRoutinoError(Routino_errno), QMessageBox::Abort);
264                 continue;
265             }
266             /* determine the profile to use for each database*/
267             QVariantMap dmap;
268             dmap["db"] = QVariant ((qulonglong)data);
269 
270             /* check possible profiles.xml locations and use the first available */
271             int pError = 0;
272             dmap["profilesPath"] = "";
273             QStringList profilesPaths = {
274                 dir.filePath(prefix + "-profiles.xml"),
275                 dir.filePath("profiles.xml"),
276                 setup->routinoPath("profiles.xml").toUtf8()
277             };
278 
279             for(const QString& profilePath : profilesPaths)
280             {
281                 QFileInfo pinfo(profilePath);
282                 if( pinfo.isReadable())
283                 {
284                     dmap["profilesPath"] = pinfo.filePath();
285                     break;
286                 }
287             }
288             if( dmap["profilesPath"].toString().isEmpty() )
289             {
290                 QMessageBox::critical(this, "Routino...", tr("Could not find a profiles XML file in expected folders. Routino Routing will not function"), QMessageBox::Ok);
291                 pError = 1;
292             }
293             else
294             {
295                 /* ensure we always reload */
296                 currentProfilesPath = "";
297                 /* check if profile will load - will abort if not good */
298                 pError = loadProfiles(dmap["profilesPath"].toString());
299             }
300             qDebug() << "Profile ... Using \n" << dmap["profilesPath"].toString();
301 
302             if( pError == 0 )
303             {
304                 comboDatabase->addItem(prefix.replace("_", " "), dmap);
305             }
306             else
307             {
308                 const QString& msg = tr(
309                     "%1\n"
310                     "Error in '%2'\n"
311                     "This needs to be fixed\n"
312                     "The associated database '%3' is ignored"
313                     ).arg(xlateRoutinoError(Routino_errno), dmap["profilesPath"].toString(), prefix);
314 
315                 QMessageBox::warning(this, "Routino...", msg, QMessageBox::Ok);
316             }
317         }
318     }
319     currentProfilesPath = "";
320 }
321 
freeDatabaseList()322 void CRouterRoutino::freeDatabaseList()
323 {
324     for(int i = 0; i < comboDatabase->count(); i++)
325     {
326         QVariantMap map = comboDatabase->itemData(i, Qt::UserRole).toMap();
327         Routino_Database* data = (Routino_Database*)(map["db"].toULongLong());
328         Routino_UnloadDatabase(data);
329     }
330     comboDatabase->clear();
331 }
332 
loadProfiles(const QString & profilesPath)333 int CRouterRoutino::loadProfiles(const QString& profilesPath)
334 {
335     int res = 0;
336     if( currentProfilesPath != profilesPath)
337     {
338         currentProfilesPath = profilesPath;
339         res = Routino_ParseXMLProfiles(profilesPath.toUtf8());
340     }
341     return res;
342 }
343 
updateHelpText()344 void CRouterRoutino::updateHelpText()
345 {
346     bool haveDB = (comboDatabase->count() != 0);
347 
348     frameHelp->setVisible(!haveDB);
349     comboDatabase->setEnabled(haveDB);
350 }
351 
calcRoute(const IGisItem::key_t & key)352 void CRouterRoutino::calcRoute(const IGisItem::key_t& key)
353 {
354     if(!mutex.tryLock())
355     {
356         return;
357     }
358 
359     try
360     {
361         QTime time;
362         time.start();
363 
364         CGisItemRte* rte = dynamic_cast<CGisItemRte*>(CGisWorkspace::self().getItemByKey(key));
365         if(nullptr == rte)
366         {
367             throw QString();
368         }
369 
370         QVariantMap map = comboDatabase->currentData(Qt::UserRole).toMap();
371         Routino_Database* data = (Routino_Database*)(map["db"].toULongLong());
372         if(nullptr == data)
373         {
374             throw QString();
375         }
376 
377         loadProfiles(map["profilesPath"].toString());
378 
379         rte->reset();
380 
381         QString strProfile = comboProfile->currentData(Qt::UserRole).toString();
382         QString strLanguage = comboLanguage->currentData(Qt::UserRole).toString();
383 
384         Routino_Profile* profile = Routino_GetProfile(strProfile.toUtf8());
385         if( profile == NULL )
386         {
387             throw tr("Required profile '%1' is not in the current profiles file.").arg(strProfile);
388         }
389         Routino_Translation* translation = Routino_GetTranslation(strLanguage.toUtf8());
390 
391         int res = Routino_ValidateProfile(data, profile);
392         if(res != 0)
393         {
394             throw xlateRoutinoError(Routino_errno);
395         }
396 
397         int options = ROUTINO_ROUTE_LIST_HTML_ALL;
398         if(comboMode->currentIndex() == 0)
399         {
400             options |= ROUTINO_ROUTE_SHORTEST;
401         }
402         if(comboMode->currentIndex() == 1)
403         {
404             options |= ROUTINO_ROUTE_QUICKEST;
405         }
406 
407         SGisLine line;
408         rte->getPolylineFromData(line);
409 
410         int idx = 0;
411         QVector<Routino_Waypoint*> waypoints(line.size(), nullptr);
412         for(const IGisLine::point_t& pt : qAsConst(line))
413         {
414             waypoints[idx] = Routino_FindWaypoint(data, profile, pt.coord.y() * RAD_TO_DEG, pt.coord.x() * RAD_TO_DEG);
415             if(waypoints[idx] == nullptr)
416             {
417                 throw xlateRoutinoError(Routino_errno);
418             }
419             idx++;
420         }
421 
422         progress = new CProgressDialog(tr("Calculate route with %1").arg(getOptions()), 0, NOINT, this);
423 
424         Routino_Output* route = Routino_CalculateRoute(data, profile, translation, waypoints.data(), waypoints.size(), options, ProgressFunc);
425 
426         delete progress;
427 
428         if(nullptr != route)
429         {
430             rte->setResult(route, getOptions() + tr("<br/>Calculation time: %1s").arg(time.elapsed() / 1000.0, 0, 'f', 2));
431             Routino_DeleteRoute(route);
432         }
433         else
434         {
435             if(Routino_errno != ROUTINO_ERROR_PROGRESS_ABORTED)
436             {
437                 throw xlateRoutinoError(Routino_errno);
438             }
439         }
440     }
441     catch(const QString& msg)
442     {
443         if(!msg.isEmpty())
444         {
445             QMessageBox::critical(this, "Routino...", msg, QMessageBox::Abort);
446         }
447     }
448 
449     mutex.unlock();
450 
451     CCanvas::triggerCompleteUpdate(CCanvas::eRedrawGis);
452 }
453 
454 
calcRoute(const QPointF & p1,const QPointF & p2,QPolygonF & coords,qreal * costs=nullptr)455 int CRouterRoutino::calcRoute(const QPointF& p1, const QPointF& p2, QPolygonF& coords, qreal* costs = nullptr)
456 {
457     if(!mutex.tryLock())
458     {
459         return -1;
460     }
461 
462     try
463     {
464         QVariantMap map = comboDatabase->currentData(Qt::UserRole).toMap();
465         Routino_Database* data = (Routino_Database*)(map["db"].toULongLong());
466         if(nullptr == data)
467         {
468             throw QString();
469         }
470 
471         loadProfiles(map["profilesPath"].toString());
472 
473         QString strProfile = comboProfile->currentData(Qt::UserRole).toString();
474         QString strLanguage = comboLanguage->currentData(Qt::UserRole).toString();
475 
476         Routino_Profile* profile = Routino_GetProfile(strProfile.toUtf8());
477         if( profile == NULL )
478         {
479             throw tr("Required profile '%1' is not in the current profiles file.").arg(strProfile);
480         }
481         Routino_Translation* translation = Routino_GetTranslation(strLanguage.toUtf8());
482 
483 
484         int res = Routino_ValidateProfile(data, profile);
485         if(res != 0)
486         {
487             throw xlateRoutinoError(Routino_errno);
488         }
489 
490         int options = ROUTINO_ROUTE_LIST_HTML_ALL;
491         if(comboMode->currentIndex() == 0)
492         {
493             options |= ROUTINO_ROUTE_SHORTEST;
494         }
495         if(comboMode->currentIndex() == 1)
496         {
497             options |= ROUTINO_ROUTE_QUICKEST;
498         }
499 
500         Routino_Waypoint* waypoints[2] = {0};
501         waypoints[0] = Routino_FindWaypoint(data, profile, p1.y() * RAD_TO_DEG, p1.x() * RAD_TO_DEG);
502         if(waypoints[0] == nullptr)
503         {
504             throw xlateRoutinoError(Routino_errno);
505         }
506 
507         waypoints[1] = Routino_FindWaypoint(data, profile, p2.y() * RAD_TO_DEG, p2.x() * RAD_TO_DEG);
508         if(waypoints[1] == nullptr)
509         {
510             throw xlateRoutinoError(Routino_errno);
511         }
512 
513         progress = new CProgressDialog(tr("Calculate route with %1").arg(getOptions()), 0, NOINT, this);
514 
515         Routino_Output* route = Routino_CalculateRoute(data, profile, translation, waypoints, 2, options, ProgressFunc);
516 
517         delete progress;
518 
519         if(route != nullptr)
520         {
521             Routino_Output* next = route;
522             while(next)
523             {
524                 if(next->type != ROUTINO_POINT_WAYPOINT)
525                 {
526                     coords << QPointF(next->lon, next->lat);
527                 }
528                 if(costs != nullptr)
529                 {
530                     if(comboMode->currentIndex() == 1)
531                     {
532                         // ROUTINO_ROUTE_QUICKEST
533                         // This works, since CRouteOptimization adapts it's weights according to the data it gets
534                         *costs = next->time;
535                     }
536                     else
537                     {
538                         // ROUTINO_ROUTE_SHORTEST
539                         *costs = next->dist;
540                     }
541                 }
542                 next = next->next;
543             }
544             Routino_DeleteRoute(route);
545         }
546         else
547         {
548             if(Routino_errno != ROUTINO_ERROR_PROGRESS_ABORTED)
549             {
550                 throw xlateRoutinoError(Routino_errno);
551             }
552             else
553             {
554                 throw QString();
555             }
556         }
557     }
558     catch(const QString& msg)
559     {
560         coords.clear();
561 
562         if(!msg.isEmpty())
563         {
564             mutex.unlock();
565             throw msg;
566         }
567     }
568 
569     mutex.unlock();
570     return coords.size();
571 }
572