File indexing completed on 2024-04-28 04:41:45
0001 /* 0002 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "../lib/geo/convexhull_p.h" 0008 #include "../lib/geo/geojson_p.h" 0009 0010 #include <QCoreApplication> 0011 #include <QDebug> 0012 #include <QDirIterator> 0013 #include <QJsonArray> 0014 #include <QJsonDocument> 0015 #include <QJsonObject> 0016 #include <QNetworkAccessManager> 0017 #include <QNetworkReply> 0018 #include <QNetworkRequest> 0019 #include <QPolygonF> 0020 #include <QRectF> 0021 0022 #include <cmath> 0023 #include <iostream> 0024 0025 class OtpProbeJob : public QObject 0026 { 0027 Q_OBJECT 0028 public: 0029 explicit OtpProbeJob(const QString &fileName, const QJsonDocument &doc, QNetworkAccessManager *nam, QObject *parent = nullptr); 0030 void start(); 0031 0032 Q_SIGNALS: 0033 void finished(); 0034 0035 private: 0036 void applySslConfig(QNetworkRequest &req); 0037 void bboxFetchDone(QNetworkReply *reply); 0038 void stopsFetchDone(QNetworkReply *reply); 0039 void writeConfigFile(); 0040 0041 QString m_configFileName; 0042 QJsonDocument m_configDoc; 0043 QNetworkAccessManager *m_nam; 0044 QUrl m_endpointUrl; 0045 QPolygonF m_boundingPolygon; 0046 QList<QSslCertificate> m_caCerts; 0047 }; 0048 0049 OtpProbeJob::OtpProbeJob(const QString &fileName, const QJsonDocument &doc, QNetworkAccessManager *nam, QObject *parent) 0050 : QObject(parent) 0051 , m_configFileName(fileName) 0052 , m_configDoc(doc) 0053 , m_nam(nam) 0054 { 0055 } 0056 0057 void OtpProbeJob::applySslConfig(QNetworkRequest &req) 0058 { 0059 if (!m_caCerts.empty()) { 0060 auto sslConfig = req.sslConfiguration(); 0061 sslConfig.setCaCertificates(m_caCerts); 0062 req.setSslConfiguration(std::move(sslConfig)); 0063 } 0064 } 0065 0066 void OtpProbeJob::start() 0067 { 0068 const auto options = m_configDoc.object().value(QLatin1String("options")).toObject(); 0069 m_endpointUrl = QUrl(options.value(QLatin1String("endpoint")).toString()); 0070 m_caCerts = QSslCertificate::fromPath(QFileInfo(m_configFileName).path() + QStringLiteral("/certs/") + options.value(QLatin1String("customCaCertificate")).toString()); 0071 auto req = QNetworkRequest(m_endpointUrl); 0072 applySslConfig(req); 0073 auto reply = m_nam->get(req); 0074 connect(reply, &QNetworkReply::finished, this, [reply, this]() { bboxFetchDone(reply); }); 0075 } 0076 0077 void OtpProbeJob::bboxFetchDone(QNetworkReply *reply) 0078 { 0079 reply->deleteLater(); 0080 if (reply->error() != QNetworkReply::NoError) { 0081 qWarning() << reply->errorString() << reply->url(); 0082 } 0083 0084 const auto desc = QJsonDocument::fromJson(reply->readAll()).object(); 0085 m_boundingPolygon = KPublicTransport::GeoJson::readOuterPolygon(desc.value(QLatin1String("polygon")).toObject()); 0086 // TODO: more elaborate outlier detection, null points is just one of the problems 0087 m_boundingPolygon.erase(std::remove_if(m_boundingPolygon.begin(), m_boundingPolygon.end(), [](auto p) { return p.isNull(); }), m_boundingPolygon.end()); 0088 0089 auto req = QNetworkRequest(QUrl(m_endpointUrl.toString() + QLatin1String("index/stops"))); 0090 applySslConfig(req); 0091 auto stopReply = m_nam->get(req); 0092 connect(stopReply, &QNetworkReply::finished, this, [stopReply, this]() { stopsFetchDone(stopReply); }); 0093 } 0094 0095 static void filterOutliers(std::vector<double> vec, double &lowerBound, double &upperBound) 0096 { 0097 std::sort(vec.begin(), vec.end()); 0098 0099 const auto n = vec.size(); 0100 const auto mean = std::accumulate(vec.begin(), vec.end(), 0.0, [n](auto a, auto b) { return a + b / n; }); 0101 auto sigma = std::accumulate(vec.begin(), vec.end(), 0.0, [n](auto a, auto b) { 0102 return a + (std::pow(b, 2.0) / n); 0103 }); 0104 sigma = std::sqrt(sigma - std::pow(mean, 2.0)) * 3.0; 0105 0106 lowerBound = mean - sigma; 0107 auto it = std::lower_bound(vec.begin(), vec.end(), lowerBound); 0108 if (it != vec.end()) { 0109 lowerBound = (*it); 0110 } 0111 upperBound = mean + sigma; 0112 it = std::lower_bound(vec.begin(), vec.end(), upperBound); 0113 if (it != vec.begin()) { 0114 upperBound = *(std::prev(it)); 0115 } 0116 0117 lowerBound = std::max(lowerBound, vec.front()); 0118 upperBound = std::min(upperBound, vec.back()); 0119 } 0120 0121 void OtpProbeJob::stopsFetchDone(QNetworkReply *reply) 0122 { 0123 reply->deleteLater(); 0124 if (reply->error() != QNetworkReply::NoError) { 0125 qWarning() << reply->errorString() << reply->url(); 0126 } 0127 0128 std::vector<QPointF> points; 0129 std::vector<double> lats, lons; 0130 const auto stops = QJsonDocument::fromJson(reply->readAll()).array(); 0131 points.reserve(stops.size()); 0132 lats.reserve(stops.size()); 0133 lons.reserve(stops.size()); 0134 for (const auto &stopV : stops) { 0135 const auto stopObj = stopV.toObject(); 0136 const auto lat = stopObj.value(QLatin1String("lat")).toDouble(); 0137 const auto lon = stopObj.value(QLatin1String("lon")).toDouble(); 0138 0139 if (std::abs(lat) < 1.0 && std::abs(lon) < 1.0) { 0140 continue; 0141 } 0142 0143 points.push_back(QPointF(lon, lat)); 0144 lats.push_back(lat); 0145 lons.push_back(lon); 0146 } 0147 0148 0149 if (lons.size() > 2 || lats.size() > 2) { 0150 double latMin, latMax, lonMin, lonMax; 0151 filterOutliers(lats, latMin, latMax); 0152 filterOutliers(lons, lonMin, lonMax); 0153 QRectF box(QPointF(lonMin, latMin), QPointF(lonMax, latMax)); 0154 0155 points.erase(std::remove_if(points.begin(), points.end(), [&box](auto p) { return !box.contains(p); }), points.end()); 0156 m_boundingPolygon = KPublicTransport::ConvexHull::compute(points); 0157 } else { 0158 qDebug() << "didn't get stop data:" << reply->url(); 0159 } 0160 0161 writeConfigFile(); 0162 } 0163 0164 void OtpProbeJob::writeConfigFile() 0165 { 0166 if (m_boundingPolygon.size() >= 4) { 0167 if (!m_boundingPolygon.isClosed()) { 0168 m_boundingPolygon.push_back(m_boundingPolygon.front()); 0169 } 0170 0171 auto obj = m_configDoc.object(); 0172 auto coverage = obj.value(QLatin1String("coverage")).toObject(); 0173 auto rtCoverage = coverage.value(QLatin1String("realtimeCoverage")).toObject(); 0174 rtCoverage.insert(QLatin1String("area"), KPublicTransport::GeoJson::writePolygon(m_boundingPolygon)); 0175 coverage.insert(QLatin1String("realtimeCoverage"), rtCoverage); 0176 obj.insert(QLatin1String("coverage"), coverage); 0177 0178 QFile f(m_configFileName); 0179 if (!f.open(QFile::WriteOnly)) { 0180 qWarning() << "Failed to open network config for writing:" << m_configFileName << f.errorString(); 0181 } else { 0182 f.write(QJsonDocument(obj).toJson()); 0183 } 0184 } 0185 0186 Q_EMIT finished(); 0187 } 0188 0189 /** Inspects OTP-based backends and queries their bounding boxes. */ 0190 int main(int argc, char **argv) 0191 { 0192 QCoreApplication app(argc, argv); 0193 if (app.arguments().size() <= 1) { 0194 std::cerr << "Usage: " << argv[0] << " [path to network configs]" << std::endl; 0195 return 1; 0196 } 0197 0198 QNetworkAccessManager nam; 0199 nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 0200 0201 int jobCount = 0; 0202 QDirIterator it(app.arguments().at(1), QDir::Files); 0203 while (it.hasNext()) { 0204 const auto fileName = it.next(); 0205 QFile f(fileName); 0206 if (!f.fileName().endsWith(QLatin1String(".json"))) { 0207 continue; 0208 } 0209 if (!f.open(QFile::ReadOnly)) { 0210 qWarning() << "Failed to open" << f.fileName() << f.errorString(); 0211 continue; 0212 } 0213 0214 const auto doc = QJsonDocument::fromJson(f.readAll()); 0215 const auto obj = doc.object(); 0216 const auto typeObj = obj.value(QLatin1String("type")).toObject(); 0217 if (!typeObj.contains(QLatin1String("otpRest")) && !typeObj.contains(QLatin1String("otpGraphQl"))) { 0218 continue; 0219 } 0220 0221 qDebug() << "Updating" << fileName; 0222 auto job = new OtpProbeJob(fileName, doc, &nam); 0223 QObject::connect(job, &OtpProbeJob::finished, &nam, [&jobCount, job]() { 0224 --jobCount; 0225 job->deleteLater(); 0226 if (jobCount == 0) { 0227 QCoreApplication::quit(); 0228 } 0229 }); 0230 ++jobCount; 0231 job->start(); 0232 } 0233 0234 return jobCount ? app.exec() : 0; 0235 } 0236 0237 #include "otpprobe.moc"