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"