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"