livedatamanager.cpp 18.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/*
    Copyright (C) 2018 Volker Krause <vkrause@kde.org>

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

18 19
#include "config-itinerary.h"

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
#include "livedatamanager.h"
#include "logging.h"
#include "pkpassmanager.h"
#include "reservationmanager.h"

#include <KItinerary/LocationUtil>
#include <KItinerary/Place>
#include <KItinerary/Reservation>
#include <KItinerary/SortUtil>
#include <KItinerary/TrainTrip>

#include <KPublicTransport/DepartureReply>
#include <KPublicTransport/DepartureRequest>
#include <KPublicTransport/Location>
#include <KPublicTransport/Manager>

36 37 38 39 40 41
#ifdef HAVE_NOTIFICATIONS
#include <KNotifications/KNotification>
#endif

#include <KLocalizedString>

Volker Krause's avatar
Volker Krause committed
42 43 44 45 46 47
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
48 49 50 51 52 53
#include <QVector>

using namespace KItinerary;

LiveDataManager::LiveDataManager(QObject *parent)
    : QObject(parent)
54

55
{
56
    m_ptMgr.reset(new KPublicTransport::Manager);
57 58 59

    m_pollTimer.setSingleShot(true);
    connect(&m_pollTimer, &QTimer::timeout, this, &LiveDataManager::poll);
60 61 62 63 64 65 66
}

LiveDataManager::~LiveDataManager() = default;

void LiveDataManager::setReservationManager(ReservationManager *resMgr)
{
    m_resMgr = resMgr;
67 68 69 70 71
    connect(resMgr, &ReservationManager::batchAdded, this, &LiveDataManager::batchAdded);
    connect(resMgr, &ReservationManager::batchChanged, this, &LiveDataManager::batchChanged);
    connect(resMgr, &ReservationManager::batchContentChanged, this, &LiveDataManager::batchChanged);
    connect(resMgr, &ReservationManager::batchRenamed, this, &LiveDataManager::batchRenamed);
    connect(resMgr, &ReservationManager::batchRemoved, this, &LiveDataManager::batchRemoved);
72

73
    const auto resIds = resMgr->batches();
74
    for (const auto &resId : resIds) {
75
        if (!isRelevant(resId)) {
76 77 78 79 80 81 82 83
            continue;
        }
        m_reservations.push_back(resId);
    }

    std::sort(m_reservations.begin(), m_reservations.end(), [this](const auto &lhs, const auto &rhs) {
        return SortUtil::isBefore(m_resMgr->reservation(lhs), m_resMgr->reservation(rhs));
    });
Volker Krause's avatar
Volker Krause committed
84 85

    loadPublicTransportData();
86
    m_pollTimer.setInterval(nextPollTime());
87 88 89 90 91 92 93
}

void LiveDataManager::setPkPassManager(PkPassManager *pkPassMgr)
{
    m_pkPassMgr = pkPassMgr;
}

94 95
void LiveDataManager::setPollingEnabled(bool pollingEnabled)
{
96 97 98 99 100 101
    if (pollingEnabled) {
        m_pollTimer.setInterval(nextPollTime());
        m_pollTimer.start();
    } else {
        m_pollTimer.stop();
    }
102 103 104 105 106 107 108
}

void LiveDataManager::setAllowInsecureServices(bool allowInsecure)
{
    m_ptMgr->setAllowInsecureBackends(allowInsecure);
}

109 110
QVariant LiveDataManager::arrival(const QString &resId)
{
111
    return QVariant::fromValue(m_arrivals.value(resId).change);
112 113
}

114 115
QVariant LiveDataManager::departure(const QString &resId)
{
116
    return QVariant::fromValue(m_departures.value(resId).change);
117
}
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

void LiveDataManager::checkForUpdates()
{
    qCDebug(Log) << m_reservations.size();
    m_pkPassMgr->updatePasses(); // TODO do this as part of the below loop

    for (auto it = m_reservations.begin(); it != m_reservations.end();) {
        const auto res = m_resMgr->reservation(*it);

        // clean up old stuff (TODO: do this a bit more precisely)
        if (SortUtil::endtDateTime(res) < QDateTime::currentDateTime().addDays(-1)) {
            it = m_reservations.erase(it);
            continue;
        }

        if (JsonLd::isA<TrainReservation>(res)) {
134
            checkTrainTrip(res, *it);
135 136
        }

137
        // TODO check for pkpass updates, for each element in this batch
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159

        ++it;
    }
}

static QString stripSpecial(const QString &str)
{
    QString res;
    res.reserve(str.size());
    std::copy_if(str.begin(), str.end(), std::back_inserter(res), [](const auto c) {
        return c.isLetter() || c.isDigit();
    });
    return res;
}

static bool isSameLine(const QString &lineName, const QString &trainName, const QString &trainNumber)
{
    const auto lhs = stripSpecial(lineName);
    const auto rhs = stripSpecial(trainName + trainNumber);
    return lhs.compare(rhs, Qt::CaseInsensitive) == 0;
}

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
static KPublicTransport::Location locationFromStation(const TrainStation &station)
{
    using namespace KPublicTransport;
    Location loc;
    loc.setName(station.name());
    loc.setCoordinate(station.geo().latitude(), station.geo().longitude());
    if (!station.identifier().isEmpty()) {
        const auto idSplit = station.identifier().split(QLatin1Char(':'));
        if (idSplit.size() == 2) {
            loc.setIdentifier(idSplit.at(0), idSplit.at(1));
        }
    }
    return loc;
}

175
void LiveDataManager::checkTrainTrip(const QVariant &res, const QString& resId)
176
{
177 178 179
    Q_ASSERT(JsonLd::isA<TrainReservation>(res));
    const auto trip = res.value<TrainReservation>().reservationFor().value<TrainTrip>();

180 181 182
    qCDebug(Log) << trip.trainName() << trip.trainNumber() << trip.departureTime();
    using namespace KPublicTransport;

183 184 185 186 187 188 189 190 191 192
    if (!hasDeparted(resId, res)) {
        DepartureRequest req(locationFromStation(trip.departureStation()));
        req.setDateTime(trip.departureTime());
        auto reply = m_ptMgr->queryDeparture(req);
        connect(reply, &Reply::finished, this, [this, trip, resId, reply]() {
            reply->deleteLater();
            if (reply->error() != Reply::NoError) {
                qCDebug(Log) << reply->error() << reply->errorString();
                return;
            }
193

194 195 196 197 198 199 200 201
            for (const auto &dep : reply->result()) {
                qCDebug(Log) << "Got departure information:" << dep.route().line().name() << dep.scheduledDepartureTime() << "for" << trip.trainNumber();
                if (dep.scheduledDepartureTime() != trip.departureTime() || !isSameLine(dep.route().line().name(), trip.trainName(), trip.trainNumber())) {
                    continue;
                }
                qCDebug(Log) << "Found departure information:" << dep.route().line().name() << dep.expectedPlatform() << dep.expectedDepartureTime();
                updateDepartureData(dep, resId);
                break;
202
            }
203 204
        });
    }
205

206 207 208 209 210 211 212 213 214 215 216
    if (!hasArrived(resId, res)) {
        DepartureRequest req(locationFromStation(trip.arrivalStation()));
        req.setMode(DepartureRequest::QueryArrival);
        req.setDateTime(trip.arrivalTime());
        auto reply = m_ptMgr->queryDeparture(req);
        connect(reply, &Reply::finished, this, [this, trip, resId, reply]() {
            reply->deleteLater();
            if (reply->error() != Reply::NoError) {
                qCDebug(Log) << reply->error() << reply->errorString();
                return;
            }
217

218 219 220 221 222 223 224 225
            for (const auto &arr : reply->result()) {
                qCDebug(Log) << "Got arrival information:" << arr.route().line().name() << arr.scheduledArrivalTime() << "for" << trip.trainNumber();
                if (arr.scheduledArrivalTime() != trip.arrivalTime() || !isSameLine(arr.route().line().name(), trip.trainName(), trip.trainNumber())) {
                    continue;
                }
                qCDebug(Log) << "Found arrival information:" << arr.route().line().name() << arr.expectedPlatform() << arr.expectedDepartureTime();
                updateArrivalData(arr, resId);
                break;
226
            }
227 228
        });
    }
229 230 231 232
}

void LiveDataManager::updateArrivalData(const KPublicTransport::Departure &arr, const QString &resId)
{
Volker Krause's avatar
Volker Krause committed
233 234
    const auto oldArr = m_arrivals.value(resId).change;
    m_arrivals.insert(resId, {arr, QDateTime::currentDateTimeUtc()});
235 236
    storePublicTransportData(resId, arr, QStringLiteral("arrival"));

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    // check if we can update static information in the reservation with what we received
    const auto res = m_resMgr->reservation(resId);
    if (JsonLd::isA<TrainReservation>(res)) {
        auto newRes = res.value<TrainReservation>();
        auto trip = res.value<TrainReservation>().reservationFor().value<TrainTrip>();
        auto station = trip.arrivalStation();
        if (!station.geo().isValid() && arr.stopPoint().hasCoordinate()) {
            station.setGeo(GeoCoordinates{arr.stopPoint().latitude(), arr.stopPoint().longitude()});
            trip.setArrivalStation(station);
        }
        if (trip.arrivalPlatform().isEmpty() && !arr.scheduledPlatform().isEmpty()) {
            trip.setArrivalPlatform(arr.scheduledPlatform());
        }
        newRes.setReservationFor(trip);

        if (res.value<TrainReservation>() != newRes) {
            m_resMgr->updateReservation(resId, newRes);
        }
    }

    // check if something changed relevant for notifications
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
    if (oldArr.arrivalDelay() == arr.arrivalDelay() && oldArr.expectedPlatform() == arr.expectedPlatform()) {
        return;
    }

#ifdef HAVE_NOTIFICATIONS
    // check if something worth notifying changed
    // ### we could do that even more clever by skipping distant future changes
    if (std::abs(oldArr.arrivalDelay() - arr.arrivalDelay()) > 2) {
        KNotification::event(KNotification::Notification,
            i18n("Delayed arrival on %1", arr.route().line().name()),
            i18n("New arrival time is: %1", QLocale().toString(arr.expectedArrivalTime().time())),
            QLatin1String("clock"));
    }
#endif

    emit departureUpdated(resId);
}

void LiveDataManager::updateDepartureData(const KPublicTransport::Departure &dep, const QString &resId)
{
278 279
    const auto oldDep = m_departures.value(resId).change;
    m_departures.insert(resId, {dep, QDateTime::currentDateTimeUtc()});
280 281
    storePublicTransportData(resId, dep, QStringLiteral("departure"));

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
    // check if we can update static information in the reservation with what we received
    const auto res = m_resMgr->reservation(resId);
    if (JsonLd::isA<TrainReservation>(res)) {
        auto newRes = res.value<TrainReservation>();
        auto trip = res.value<TrainReservation>().reservationFor().value<TrainTrip>();
        auto station = trip.departureStation();
        if (!station.geo().isValid() && dep.stopPoint().hasCoordinate()) {
            station.setGeo(GeoCoordinates{dep.stopPoint().latitude(), dep.stopPoint().longitude()});
            trip.setDeparatureStation(station);
        }
        if (trip.departurePlatform().isEmpty() && !dep.scheduledPlatform().isEmpty()) {
            trip.setDeparturePlatform(dep.scheduledPlatform());
        }
        newRes.setReservationFor(trip);

        if (res.value<TrainReservation>() != newRes) {
            m_resMgr->updateReservation(resId, newRes);
        }
    }

    // check if something changed relevant for notification
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    if (oldDep.departureDelay() == dep.departureDelay() && oldDep.expectedPlatform() == dep.expectedPlatform()) {
        return;
    }

#ifdef HAVE_NOTIFICATIONS
    // check if something worth notifying changed
    // ### we could do that even more clever by skipping distant future changes
    if (std::abs(oldDep.departureDelay() - dep.departureDelay()) > 2) {
        KNotification::event(KNotification::Notification,
            i18n("Delayed departure on %1", dep.route().line().name()),
            i18n("New departure time is: %1", QLocale().toString(dep.expectedDepartureTime().time())),
            QLatin1String("clock"));
    }

    if (oldDep.expectedPlatform() != dep.expectedPlatform() && dep.scheduledPlatform() != dep.expectedPlatform()) {
        KNotification::event(KNotification::Notification,
            i18n("Platform change on %1", dep.route().line().name()),
            i18n("New departure platform is: %1", dep.expectedPlatform()),
            QLatin1String("clock"));
    }
#endif
324

325
    emit departureUpdated(resId);
326
}
327

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
bool LiveDataManager::hasDeparted(const QString &resId, const QVariant &res) const
{
    const auto now = QDateTime::currentDateTime();

    if (JsonLd::isA<TrainTrip>(res)) {
        const auto &dep = m_departures.value(resId).change;
        if (dep.hasExpectedDepartureTime()) {
            return dep.expectedDepartureTime() < now;
        }
    }

    return SortUtil::startDateTime(res) < now;
}

bool LiveDataManager::hasArrived(const QString &resId, const QVariant &res) const
{
    const auto now = QDateTime::currentDateTime();

    if (JsonLd::isA<TrainTrip>(res)) {
        const auto &arr = m_arrivals.value(resId).change;
        if (arr.hasExpectedArrivalTime()) {
            return arr.expectedArrivalTime() < now;
        }
    }

    return SortUtil::endtDateTime(res) < now;
}

356
void LiveDataManager::loadPublicTransportData(const QString &prefix, QHash<QString, TrainChange> &data) const
Volker Krause's avatar
Volker Krause committed
357 358 359 360 361 362 363 364 365 366 367 368 369 370
{
    const auto basePath = QString(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/publictransport/"));
    QDirIterator it(basePath + prefix, QDir::Files | QDir::NoSymLinks);
    while (it.hasNext()) {
        it.next();
        const auto resId = it.fileInfo().baseName();
        if (std::find(m_reservations.begin(), m_reservations.end(), resId) == m_reservations.end()) {
            QDir(it.path()).remove(it.fileName());
        } else {
            QFile f(it.filePath());
            if (!f.open(QFile::ReadOnly)) {
                qCWarning(Log) << "Failed to load public transport file" << f.fileName() << f.errorString();
                continue;
            }
371
            data.insert(resId, {KPublicTransport::Departure::fromJson(QJsonDocument::fromJson(f.readAll()).object()), f.fileTime(QFile::FileModificationTime)});
Volker Krause's avatar
Volker Krause committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
        }
    }
}

void LiveDataManager::loadPublicTransportData()
{
    loadPublicTransportData(QStringLiteral("arrival"), m_arrivals);
    loadPublicTransportData(QStringLiteral("departure"), m_departures);
}

void LiveDataManager::storePublicTransportData(const QString &resId, const KPublicTransport::Departure &dep, const QString &type) const
{
    const auto basePath = QString(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/publictransport/")
        + type + QLatin1Char('/'));
    QDir().mkpath(basePath);

    QFile file(basePath + resId + QLatin1String(".json"));
    if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
        qCWarning(Log) << "Failed to open public transport cache file:" << file.fileName() << file.errorString();
        return;
    }
    file.write(QJsonDocument(KPublicTransport::Departure::toJson(dep)).toJson());
}

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
bool LiveDataManager::isRelevant(const QString &resId) const
{
    const auto res = m_resMgr->reservation(resId);
    if (!LocationUtil::isLocationChange(res)) { // we only care about transit reservations
        return false;
    }
    if (SortUtil::endtDateTime(res) < QDateTime::currentDateTime().addDays(-1)) {
        return false; // we don't care about past events
    }

    // we can only handle train trips and reservations with pkpass files so far
    if (!JsonLd::canConvert<Reservation>(res)) {
        return false;
    }
    return JsonLd::isA<TrainReservation>(res) || !JsonLd::convert<Reservation>(res).pkpassSerialNumber().isEmpty();
}

413
void LiveDataManager::batchAdded(const QString &resId)
414 415 416 417 418 419 420 421
{
    if (!isRelevant(resId)) {
        return;
    }

    // TODO
}

422
void LiveDataManager::batchChanged(const QString &resId)
423 424 425 426
{
    // TODO
}

427 428 429 430 431 432 433 434 435
void LiveDataManager::batchRenamed(const QString &oldBatchId, const QString &newBatchId)
{
    // ### we can do this more efficiently
    batchRemoved(oldBatchId);
    batchAdded(newBatchId);
}


void LiveDataManager::batchRemoved(const QString &resId)
436 437 438 439 440 441 442
{
    const auto it = std::find(m_reservations.begin(), m_reservations.end(), resId);
    if (it != m_reservations.end()) {
        m_reservations.erase(it);
    }
}

443 444 445
void LiveDataManager::poll()
{
    qCDebug(Log);
446 447 448 449 450 451
    for (const auto &resId : m_reservations) {
        if (nextPollTimeForReservation(resId) > 60 * 1000) {
            continue;
        }
        const auto res = m_resMgr->reservation(resId);
        if (JsonLd::isA<TrainReservation>(res)) {
452
            checkTrainTrip(res, resId);
453 454 455 456 457
        }
    }

    m_pollTimer.setInterval(std::max(nextPollTime(), 60 * 1000)); // we pool everything that happens within a minute here
    m_pollTimer.start();
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
}

int LiveDataManager::nextPollTime() const
{
    int t = std::numeric_limits<int>::max();
    for (const auto &resId : m_reservations) {
        t = std::min(t, nextPollTimeForReservation(resId));
    }
    qCDebug(Log) << "next auto-update in" << (t/1000) << "secs";
    return t;
}

struct {
    int distance; // secs
    int pollInterval; // secs
} static const pollIntervalTable[] = {
    { 3600, 5*60 }, // for <1h we poll every 5 minutes
    { 4 * 3600, 15 * 60 }, // for <4h we poll every 15 minutes
    { 24 * 3600, 3600 }, // for <1d we poll once per hour
    { 4 * 24 * 3600, 24 * 3600 }, // for <4d we poll once per day
};

int LiveDataManager::nextPollTimeForReservation(const QString& resId) const
{
    const auto res = m_resMgr->reservation(resId);

    const auto now = QDateTime::currentDateTime();
    auto dist = now.secsTo(SortUtil::startDateTime(res));
    if (dist < 0) {
        dist = now.secsTo(SortUtil::endtDateTime(res));
    }
    if (dist < 0) {
        return std::numeric_limits<int>::max();
    }

    // TODO consider delayed but still pending departures/arrivals too

    const auto it = std::lower_bound(std::begin(pollIntervalTable), std::end(pollIntervalTable), dist, [](const auto &lhs, const auto rhs) {
        return lhs.distance < rhs;
    });
    if (it == std::end(pollIntervalTable)) {
        return std::numeric_limits<int>::max();
    }

502 503 504
    // check last poll time for this reservation
    const auto lastArrivalPoll = m_arrivals.value(resId).timestamp;
    const auto lastDeparturePoll = m_departures.value(resId).timestamp;
505 506 507 508 509 510 511 512
    auto lastRelevantPoll = lastArrivalPoll;
    // ignore departure if we have already departed
    if (!hasDeparted(resId, res) && lastDeparturePoll.isValid()) {
        if (!lastArrivalPoll.isValid() || lastArrivalPoll > lastDeparturePoll) {
            lastRelevantPoll = lastDeparturePoll;
        }
    }
    const int lastPollDist = !lastRelevantPoll.isValid()
513
        ? (24 * 3600) // no poll yet == long time ago
514 515
        : lastRelevantPoll.secsTo(now);
    return std::max((it->pollInterval - lastPollDist) * 1000, 0); // we need msecs
516 517
}

518
#include "moc_livedatamanager.cpp"