File indexing completed on 2024-05-12 04:42:07

0001 /*
0002     SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include <config-editorcontroller.h>
0007 
0008 #include "editorcontroller.h"
0009 #include "logging.h"
0010 
0011 #if HAVE_KSERVICE
0012 #include <KService>
0013 #include <KShell>
0014 #endif
0015 
0016 #ifdef Q_OS_ANDROID
0017 #include <QJniObject>
0018 #endif
0019 
0020 #include <QCoreApplication>
0021 #include <QDesktopServices>
0022 #include <QElapsedTimer>
0023 #include <QHostAddress>
0024 #include <QNetworkAccessManager>
0025 #include <QNetworkReply>
0026 #include <QNetworkRequest>
0027 #include <QProcess>
0028 #include <QTcpSocket>
0029 #include <QTimer>
0030 #include <QUrl>
0031 #include <QUrlQuery>
0032 
0033 using namespace KOSM;
0034 
0035 // https://github.com/openstreetmap/iD/blob/develop/API.md
0036 static void openElementInId(OSM::Element element)
0037 {
0038     QUrl url;
0039     url.setScheme(QStringLiteral("https"));
0040     url.setHost(QStringLiteral("www.openstreetmap.org"));
0041     url.setPath(QStringLiteral("/edit"));
0042 
0043     QUrlQuery query;
0044     query.addQueryItem(QStringLiteral("editor"), QStringLiteral("id"));
0045     switch (element.type()) {
0046         case OSM::Type::Null:
0047             Q_UNREACHABLE();
0048         case OSM::Type::Node:
0049             query.addQueryItem(QStringLiteral("node"), QString::number(element.id()));
0050             break;
0051         case OSM::Type::Way:
0052             query.addQueryItem(QStringLiteral("way"), QString::number(element.id()));
0053             break;
0054         case OSM::Type::Relation:
0055             query.addQueryItem(QStringLiteral("relation"), QString::number(element.id()));
0056             break;
0057     }
0058 
0059     url.setQuery(query);
0060     qCDebug(EditorLog) << url;
0061     QDesktopServices::openUrl(url);
0062 }
0063 
0064 static void openBoundBoxInId(OSM::BoundingBox box)
0065 {
0066     QUrl url;
0067     url.setScheme(QStringLiteral("https"));
0068     url.setHost(QStringLiteral("www.openstreetmap.org"));
0069     url.setPath(QStringLiteral("/edit"));
0070 
0071     QUrlQuery query;
0072     query.addQueryItem(QStringLiteral("editor"), QStringLiteral("id"));
0073     query.addQueryItem(QStringLiteral("lat"), QString::number(box.center().latF()));
0074     query.addQueryItem(QStringLiteral("lon"), QString::number(box.center().lonF()));
0075     query.addQueryItem(QStringLiteral("zoom"), QStringLiteral("17")); // TODO compute zoom based on box size
0076 
0077     url.setQuery(query);
0078     QDesktopServices::openUrl(url);
0079 }
0080 
0081 static QUrl makeJosmLoadAndZoomCommand(OSM::BoundingBox box, OSM::Element element)
0082 {
0083     QUrl url;
0084     url.setPath(QStringLiteral("/load_and_zoom"));
0085 
0086     QUrlQuery query;
0087     // ensure bbox is not 0x0 for nodes
0088     query.addQueryItem(QStringLiteral("left"), QString::number(box.min.lonF() - 0.0001));
0089     query.addQueryItem(QStringLiteral("bottom"), QString::number(box.min.latF() - 0.0001));
0090     query.addQueryItem(QStringLiteral("right"), QString::number(box.max.lonF() + 0.0001));
0091     query.addQueryItem(QStringLiteral("top"), QString::number(box.max.latF() + 0.0001));
0092 
0093     switch (element.type()) {
0094         case OSM::Type::Null:
0095             break;
0096         case OSM::Type::Node:
0097             query.addQueryItem(QStringLiteral("select"), QLatin1String("node") + QString::number(element.id()));
0098             break;
0099         case OSM::Type::Way:
0100             query.addQueryItem(QStringLiteral("select"), QLatin1String("way") + QString::number(element.id()));
0101             break;
0102         case OSM::Type::Relation:
0103             query.addQueryItem(QStringLiteral("select"), QLatin1String("relation") + QString::number(element.id()));
0104             break;
0105     }
0106 
0107     url.setQuery(query);
0108     return url;
0109 }
0110 
0111 #ifdef Q_OS_ANDROID
0112 // https://vespucci.io/tutorials/vespucci_intents/
0113 static void openVespucci(OSM::BoundingBox box, OSM::Element element = {})
0114 {
0115     auto url = makeJosmLoadAndZoomCommand(box, element);
0116     url.setScheme(QStringLiteral("josm"));
0117     qCDebug(EditorLog) << url;
0118     QDesktopServices::openUrl(url);
0119 }
0120 
0121 #else
0122 
0123 static std::unique_ptr<QNetworkAccessManager> s_nam;
0124 
0125 static void josmRemoteCommand(const QUrl &url, QElapsedTimer timeout)
0126 {
0127    if (!s_nam) {
0128         s_nam = std::make_unique<QNetworkAccessManager>();
0129     }
0130     auto reply = s_nam->get(QNetworkRequest(url));
0131     QObject::connect(reply, &QNetworkReply::finished, QCoreApplication::instance(), [reply, url, timeout]() {
0132         reply->deleteLater();
0133         qCDebug(EditorLog) << reply->errorString();
0134         qCDebug(EditorLog) << reply->readAll();
0135         // retry in case JOSM is still starting up
0136         if (reply->error() != QNetworkReply::NoError && timeout.elapsed() < 30000) {
0137             QTimer::singleShot(1000, QCoreApplication::instance(), [url, timeout]() { josmRemoteCommand(url, timeout); });
0138         }
0139     });
0140 }
0141 
0142 // https://josm.openstreetmap.de/wiki/Help/RemoteControlCommands
0143 static void openJosm(OSM::BoundingBox box, OSM::Element element = {})
0144 {
0145 #if HAVE_KSERVICE
0146     QTcpSocket socket;
0147     socket.connectToHost(QHostAddress::LocalHost, 8111);
0148     if (!socket.waitForConnected(100)) {
0149         qCDebug(EditorLog) << "JOSM not running yet, or doesn't have remote control enabled." << socket.errorString();
0150         auto s = KService::serviceByDesktopName(QStringLiteral("org.openstreetmap.josm"));
0151         qCDebug(EditorLog) << "JOSM not running yet, or doesn't have remote control enabled." << s->exec();
0152         Q_ASSERT(s);
0153         auto args = KShell::splitArgs(s->exec());
0154         if (args.isEmpty()) {
0155             return;
0156         }
0157         const auto program = args.takeFirst();
0158         QProcess::startDetached(program, args);
0159     }
0160     socket.close();
0161 #endif
0162 
0163     auto url = makeJosmLoadAndZoomCommand(box, element);
0164     url.setScheme(QStringLiteral("http"));
0165     url.setHost(QStringLiteral("127.0.0.1"));
0166     url.setPort(8111);
0167     qCDebug(EditorLog) << url;
0168 
0169     QElapsedTimer timeout;
0170     timeout.start();
0171     josmRemoteCommand(url, timeout);
0172 }
0173 #endif
0174 
0175 bool EditorController::hasEditor(Editor editor)
0176 {
0177     switch (editor) {
0178         case ID:
0179             return true;
0180         case JOSM:
0181 #if HAVE_KSERVICE
0182         {
0183             auto s = KService::serviceByDesktopName(QStringLiteral("org.openstreetmap.josm"));
0184             return s;
0185         }
0186 #else
0187             return false;
0188 #endif
0189         case Vespucci:
0190 #ifdef Q_OS_ANDROID
0191             return QJniObject::callStaticMethod<jboolean>("org.kde.osm.editorcontroller.EditorController",
0192                 "hasVespucci", "(Landroid/content/Context;)Z", QNativeInterface::QAndroidApplication::context());
0193 #else
0194             return false;
0195 #endif
0196     }
0197 
0198     return false;
0199 }
0200 
0201 void EditorController::editElement(OSM::Element element, Editor editor)
0202 {
0203     if (element.type() == OSM::Type::Null) {
0204         return;
0205     }
0206 
0207     qCDebug(EditorLog) << element.url() << editor;
0208     switch (editor) {
0209         case ID:
0210             openElementInId(element);
0211             break;
0212         case JOSM:
0213 #ifndef Q_OS_ANDROID
0214             openJosm(element.boundingBox(), element);
0215 #endif
0216             break;
0217         case Vespucci:
0218 #ifdef Q_OS_ANDROID
0219             openVespucci(element.boundingBox(), element);
0220 #endif
0221             break;
0222     }
0223 }
0224 
0225 void EditorController::editBoundingBox(OSM::BoundingBox box, Editor editor)
0226 {
0227     qCDebug(EditorLog) << box << editor;
0228     switch (editor) {
0229         case ID:
0230             openBoundBoxInId(box);
0231             break;
0232         case JOSM:
0233 #ifndef Q_OS_ANDROID
0234             openJosm(box);
0235 #endif
0236             break;
0237         case Vespucci:
0238 #ifdef Q_OS_ANDROID
0239             openVespucci(box);
0240 #endif
0241             break;
0242     }
0243 }
0244 
0245 #include "moc_editorcontroller.cpp"