diff --git a/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.cpp b/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.cpp index eb8c6d5e..6dfb87dc 100644 --- a/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.cpp +++ b/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.cpp @@ -1,201 +1,191 @@ /* Copyright (c) 2017 Volker Krause This library 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 library 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 Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "extractorpostprocessor.h" +#include "calendarhandler.h" #include "datatypes.h" #include "jsonlddocument.h" #include "airportdb/airportdb.h" #include #include #include void ExtractorPostprocessor::process(const QVector &data) { m_data.reserve(data.size()); for (auto d : data) { if (d.userType() == qMetaTypeId()) { d = processFlightReservation(d); } if (filterReservation(d)) { m_data.push_back(d); } } std::stable_sort(m_data.begin(), m_data.end(), [](const QVariant &lhs, const QVariant &rhs) { - return startDateTime(lhs) < startDateTime(rhs); + return CalendarHandler::startDateTime(lhs) < CalendarHandler::startDateTime(rhs); }); } QVector ExtractorPostprocessor::result() const { return m_data; } QVariant ExtractorPostprocessor::processProperty(QVariant obj, const char *name, QVariant (ExtractorPostprocessor::*processor)(QVariant) const) const { auto value = JsonLdDocument::readProperty(obj, name); value = (this->*processor)(value); JsonLdDocument::writeProperty(obj, name, value); return obj; } QVariant ExtractorPostprocessor::processFlightReservation(QVariant res) const { return processProperty(res, "reservationFor", &ExtractorPostprocessor::processFlight); } QVariant ExtractorPostprocessor::processFlight(QVariant flight) const { flight = processProperty(flight, "departureAirport", &ExtractorPostprocessor::processAirport); flight = processProperty(flight, "arrivalAirport", &ExtractorPostprocessor::processAirport); processFlightTime(flight, "boardingTime", "departureAirport"); processFlightTime(flight, "departureTime", "departureAirport"); processFlightTime(flight, "arrivalTime", "arrivalAirport"); return flight; } QVariant ExtractorPostprocessor::processAirport(QVariant airport) const { // complete missing IATA codes auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (iataCode.isEmpty()) { iataCode = AirportDb::iataCodeFromName(JsonLdDocument::readProperty(airport, "name").toString()).toString(); if (!iataCode.isEmpty()) { JsonLdDocument::writeProperty(airport, "iataCode", iataCode); } } // complete missing geo coordinates auto geo = JsonLdDocument::readProperty(airport, "geo"); if (!geo.value().isValid()) { const auto coord = AirportDb::coordinateForAirport(AirportDb::IataCode{iataCode}); if (coord.isValid()) { geo = QVariant::fromValue(GeoCoordinates()); JsonLdDocument::writeProperty(geo, "latitude", coord.latitude); JsonLdDocument::writeProperty(geo, "longitude", coord.longitude); JsonLdDocument::writeProperty(airport, "geo", geo); } } return airport; } void ExtractorPostprocessor::processFlightTime(QVariant &flight, const char *timePropName, const char *airportPropName) const { const auto airport = JsonLdDocument::readProperty(flight, airportPropName); const auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (iataCode.isEmpty()) { return; } auto dt = JsonLdDocument::readProperty(flight, timePropName).toDateTime(); if (!dt.isValid() || dt.timeSpec() == Qt::TimeZone) { return; } const auto tz = AirportDb::timezoneForAirport(AirportDb::IataCode{iataCode}); if (!tz.isValid()) { return; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return; } dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); // if we updated from UTC offset to timezone spec here, QDateTime will compare equal // and the auto-generated property code will not actually update the property // so, clear the property first to force an update JsonLdDocument::writeProperty(flight, timePropName, QDateTime()); JsonLdDocument::writeProperty(flight, timePropName, dt); } bool ExtractorPostprocessor::filterReservation(const QVariant &res) const { const auto resFor = JsonLdDocument::readProperty(res, "reservationFor"); if (resFor.isNull()) { return false; } if (resFor.userType() == qMetaTypeId()) { return filterFlight(resFor); } else if (resFor.userType() == qMetaTypeId()) { return filterTrainTrip(resFor); } if (res.userType() == qMetaTypeId()) { return filterLodgingReservation(res); } return true; } bool ExtractorPostprocessor::filterLodgingReservation(const QVariant& res) const { const auto checkinDate = JsonLdDocument::readProperty(res, "checkinDate").toDateTime(); const auto checkoutDate = JsonLdDocument::readProperty(res, "checkoutDate").toDateTime(); return checkinDate.isValid() && checkoutDate.isValid(); } bool ExtractorPostprocessor::filterFlight(const QVariant &flight) const { const auto depDt = JsonLdDocument::readProperty(flight, "departureTime").toDateTime(); const auto arrDt = JsonLdDocument::readProperty(flight, "arrivalTime").toDateTime(); return filterAirport(JsonLdDocument::readProperty(flight, "departureAirport")) && filterAirport(JsonLdDocument::readProperty(flight, "arrivalAirport")) && depDt.isValid() && arrDt.isValid(); } bool ExtractorPostprocessor::filterAirport(const QVariant &airport) const { const auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); const auto name = JsonLdDocument::readProperty(airport, "name").toString(); return !iataCode.isEmpty() || !name.isEmpty(); } bool ExtractorPostprocessor::filterTrainTrip(const QVariant &trip) const { const auto depDt = JsonLdDocument::readProperty(trip, "departureTime").toDateTime(); const auto arrDt = JsonLdDocument::readProperty(trip, "arrivalTime").toDateTime(); return filterAirport(JsonLdDocument::readProperty(trip, "departureStation")) && filterAirport(JsonLdDocument::readProperty(trip, "arrivalStation")) && depDt.isValid() && arrDt.isValid(); } bool ExtractorPostprocessor::filterTrainStation(const QVariant &station) const { return !JsonLdDocument::readProperty(station, "name").toString().isEmpty(); } - -QDateTime ExtractorPostprocessor::startDateTime(const QVariant &res) -{ - if (res.userType() == qMetaTypeId() || res.userType() == qMetaTypeId()) { - const auto trip = JsonLdDocument::readProperty(res, "reservationFor"); - return JsonLdDocument::readProperty(trip, "departureTime").toDateTime(); - } else if (res.userType() == qMetaTypeId()) { - return JsonLdDocument::readProperty(res, "checkinDate").toDateTime(); - } - return {}; -} diff --git a/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.h b/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.h index eb2431fb..c515da19 100644 --- a/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.h +++ b/plugins/messageviewer/bodypartformatter/semantic/extractorpostprocessor.h @@ -1,53 +1,51 @@ /* Copyright (c) 2017 Volker Krause This library 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 library 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 Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EXTRACTORPOSTPROCESSOR_H #define EXTRACTORPOSTPROCESSOR_H #include #include /** Post-process extracted data to filter out garbage and augment data from other sources. */ class ExtractorPostprocessor { public: void process(const QVector &data); QVector result() const; private: QVariant processProperty(QVariant obj, const char *name, QVariant (ExtractorPostprocessor::*processor)(QVariant) const) const; QVariant processFlightReservation(QVariant res) const; QVariant processFlight(QVariant flight) const; QVariant processAirport(QVariant airport) const; void processFlightTime(QVariant &flight, const char *timePropName, const char *airportPropName) const; bool filterReservation(const QVariant &res) const; bool filterLodgingReservation(const QVariant &res) const; bool filterFlight(const QVariant &flight) const; bool filterAirport(const QVariant &airport) const; bool filterTrainTrip(const QVariant &trip) const; bool filterTrainStation(const QVariant &station) const; - static QDateTime startDateTime(const QVariant &res); - QVector m_data; }; #endif // EXTRACTORPOSTPROCESSOR_H diff --git a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp index 0eeaef9f..89d540e3 100644 --- a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp +++ b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp @@ -1,260 +1,246 @@ /* Copyright (c) 2017 Volker Krause This library 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 library 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 Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "semanticurlhandler.h" #include "calendarhandler.h" #include "datatypes.h" #include "jsonlddocument.h" #include "semanticmemento.h" #include "semantic_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include bool SemanticUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(viewerInstance); Q_UNUSED(part); return path == QLatin1String("semanticAction"); } static void addGoToMapAction(QMenu *menu, const QVariant &place) { if (place.isNull()) { return; } const auto addr = JsonLdDocument::readProperty(place, "address"); if (!addr.isNull()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", JsonLdDocument::readProperty(place, "name").toString())); QObject::connect(action, &QAction::triggered, menu, [addr]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/search")); const QString queryString = JsonLdDocument::readProperty(addr, "streetAddress").toString() + QLatin1String(", ") + JsonLdDocument::readProperty(addr, "postalCode").toString() + QLatin1Char(' ') + JsonLdDocument::readProperty(addr, "addressLocality").toString() + QLatin1String(", ") + JsonLdDocument::readProperty(addr, "addressCountry").toString(); QUrlQuery query; query.addQueryItem(QStringLiteral("query"), queryString); url.setQuery(query); QDesktopServices::openUrl(url); }); return; } const auto geo = JsonLdDocument::readProperty(place, "geo"); if (!geo.isNull()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", JsonLdDocument::readProperty(place, "name").toString())); // zoom out further from airports, they are larger and you usually want to go further away from them const auto zoom = place.userType() == qMetaTypeId() ? 12 : 16; QObject::connect(action, &QAction::triggered, menu, [geo, zoom]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/")); const QString fragment = QLatin1String("map=") + QString::number(zoom) + QLatin1Char('/') + JsonLdDocument::readProperty(geo, "latitude").toString() + QLatin1Char('/') + JsonLdDocument::readProperty(geo, "longitude").toString(); url.setFragment(fragment); QDesktopServices::openUrl(url); }); } } bool SemanticUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const { Q_UNUSED(part); if (path != QLatin1String("semanticAction")) { return false; } const auto m = memento(part); if (!m || m->isEmpty()) { return false; } const auto date = dateForReservation(m); QMenu menu; QAction *action = nullptr; if (date.isValid()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("view-calendar")), i18n("Show Calendar")); QObject::connect(action, &QAction::triggered, this, [this, date](){ showCalendar(date); }); } action = menu.addAction(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("Add To Calendar")); QObject::connect(action, &QAction::triggered, this, [this, m](){ addToCalendar(m); }); QSet places; for (const auto &r : m->data()) { if (r.userType() == qMetaTypeId()) { addGoToMapAction(&menu, JsonLdDocument::readProperty(r, "reservationFor")); } else if (r.userType() == qMetaTypeId()) { const auto flight = JsonLdDocument::readProperty(r, "reservationFor"); auto airport = JsonLdDocument::readProperty(flight, "departureAirport"); auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (!places.contains(iataCode)) { addGoToMapAction(&menu, airport); places.insert(iataCode); } airport = JsonLdDocument::readProperty(flight, "arrivalAirport"); iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (!places.contains(iataCode)) { addGoToMapAction(&menu, airport); places.insert(iataCode); } } else if (r.userType() == qMetaTypeId()) { const auto trip = JsonLdDocument::readProperty(r, "reservationFor"); auto station = JsonLdDocument::readProperty(trip, "departureStation"); auto name = JsonLdDocument::readProperty(station, "name").toString(); if (!places.contains(name)) { addGoToMapAction(&menu, station); places.insert(name); } station = JsonLdDocument::readProperty(trip, "arrivalStation"); name = JsonLdDocument::readProperty(station, "name").toString(); if (!places.contains(name)) { addGoToMapAction(&menu, station); places.insert(name); } } } menu.exec(p); return true; } QString SemanticUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(part); if (path == QLatin1String("semanticAction")) { return i18n("Add reservation to your calendar."); } return {}; } SemanticMemento *SemanticUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const { const auto node = part->content()->topLevel(); const auto nodeHelper = part->nodeHelper(); if (!nodeHelper || !node) { return nullptr; } return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), "org.kde.messageviewer.semanticData")); } QDate SemanticUrlHandler::dateForReservation(SemanticMemento *memento) const { for (const auto &r : memento->data()) { - if (r.userType() == qMetaTypeId()) { - const auto v = JsonLdDocument::readProperty(r, "reservationFor"); - const auto d = JsonLdDocument::readProperty(v, "departureTime").toDate(); - if (d.isValid()) { - return d; - } - } else if (r.userType() == qMetaTypeId()) { - const auto d = JsonLdDocument::readProperty(r, "checkinDate").toDate(); - if (d.isValid()) { - return d; - } - } else if (r.userType() == qMetaTypeId()) { - const auto trip = JsonLdDocument::readProperty(r, "reservationFor"); - const auto d = JsonLdDocument::readProperty(trip, "departureTime").toDate(); - if (d.isValid()) { - return d; - } + const auto dt = CalendarHandler::startDateTime(r); + if (dt.isValid()) { + return dt.date(); } } return {}; } void SemanticUrlHandler::showCalendar(const QDate &date) const { // ensure KOrganizer or Kontact are running QString error, dbusService; const auto result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), {}, &error, &dbusService) == 0; if (!result) { qCWarning(SEMANTIC_LOG) << "Failed to start KOrganizer" << error << dbusService; } // switch to KOrganizer if we are using Kontact std::unique_ptr kontactIface( new QDBusInterface(QStringLiteral("org.kde.kontact"), QStringLiteral("/KontactInterface"), QStringLiteral("org.kde.kontact.KontactInterface"), QDBusConnection::sessionBus())); if (kontactIface->isValid()) { kontactIface->call(QStringLiteral("selectPlugin"), QStringLiteral("kontact_korganizerplugin")); } // select the date of the reservation std::unique_ptr korgIface( new QDBusInterface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QStringLiteral("org.kde.Korganizer.Calendar"), QDBusConnection::sessionBus())); if (!korgIface->isValid()) { qCWarning(SEMANTIC_LOG) << "Calendar interface is not valid! " << korgIface->lastError().message(); return; } korgIface->call(QStringLiteral("showEventView")); korgIface->call(QStringLiteral("showDate"), date); } void SemanticUrlHandler::addToCalendar(SemanticMemento *memento) const { using namespace KCalCore; const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &r : memento->data()) { auto event = CalendarHandler::findEvent(calendar, r); if (!event) { event.reset(new Event); CalendarHandler::fillEvent(r, event); if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) { continue; } calendar->addEvent(event); } else { CalendarHandler::fillEvent(r, event); calendar->modifyIncidence(event); } } }