File indexing completed on 2025-03-09 04:45:32

0001 /*
0002     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "favoritelocationmodel.h"
0008 
0009 #include "gpxexport.h"
0010 #include "json.h"
0011 #include "jsonio.h"
0012 #include "logging.h"
0013 
0014 #include "gpx/gpxreader.h"
0015 
0016 #include <KItinerary/LocationUtil>
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QDebug>
0021 #include <QDir>
0022 #include <QJsonArray>
0023 #include <QFile>
0024 #include <QSettings>
0025 #include <QStandardPaths>
0026 
0027 #include <cmath>
0028 
0029 class FavoriteLocationPrivate : public QSharedData
0030 {
0031 public:
0032     QString name;
0033     float latitude = NAN;
0034     float longitude = NAN;
0035 };
0036 
0037 FavoriteLocation::FavoriteLocation()
0038     : d(new FavoriteLocationPrivate)
0039 {
0040 }
0041 
0042 FavoriteLocation::FavoriteLocation(const FavoriteLocation&) = default;
0043 FavoriteLocation::FavoriteLocation(FavoriteLocation &&) = default;
0044 FavoriteLocation::~FavoriteLocation() = default;
0045 FavoriteLocation& FavoriteLocation::operator=(const FavoriteLocation&) = default;
0046 
0047 bool FavoriteLocation::isValid() const
0048 {
0049     return !d->name.isEmpty() && !std::isnan(d->latitude) && !std::isnan(d->longitude);
0050 }
0051 
0052 QString FavoriteLocation::name() const
0053 {
0054     return d->name;
0055 }
0056 
0057 void FavoriteLocation::setName(const QString &name)
0058 {
0059     d.detach();
0060     d->name = name;
0061 }
0062 
0063 float FavoriteLocation::latitude() const
0064 {
0065     return d->latitude;
0066 }
0067 
0068 void FavoriteLocation::setLatitude(float lat)
0069 {
0070     d.detach();
0071     d->latitude = lat;
0072 }
0073 
0074 float FavoriteLocation::longitude() const
0075 {
0076     return d->longitude;
0077 }
0078 
0079 void FavoriteLocation::setLongitude(float lon)
0080 {
0081     d.detach();
0082     d->longitude = lon;
0083 }
0084 
0085 FavoriteLocation FavoriteLocation::fromJson(const QJsonObject& obj)
0086 {
0087     return Json::fromJson<FavoriteLocation>(obj);
0088 }
0089 
0090 std::vector<FavoriteLocation> FavoriteLocation::fromJson(const QJsonArray &array)
0091 {
0092     return Json::fromJson<FavoriteLocation>(array);
0093 }
0094 
0095 QJsonObject FavoriteLocation::toJson(const FavoriteLocation& loc)
0096 {
0097     return Json::toJson(loc);
0098 }
0099 
0100 QJsonArray FavoriteLocation::toJson(const std::vector<FavoriteLocation> &locs)
0101 {
0102     return Json::toJson(locs);
0103 }
0104 
0105 
0106 static QString favoriteLocationPath()
0107 {
0108     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/favorite-locations/");
0109 }
0110 
0111 FavoriteLocationModel::FavoriteLocationModel(QObject *parent)
0112     : QAbstractListModel(parent)
0113 {
0114     // load existing locations
0115     QFile f(favoriteLocationPath() + QLatin1StringView("locations.json"));
0116     if (f.open(QFile::ReadOnly)) { // error is fine, file might not exist yet
0117         const auto val = JsonIO::read(f.readAll());
0118         beginResetModel();
0119         m_locations = FavoriteLocation::fromJson(val.toArray());
0120         endResetModel();
0121     }
0122 
0123     // migrate old home configuration
0124     if (m_locations.empty()) {
0125         FavoriteLocation home;
0126         QSettings settings;
0127         settings.beginGroup(QStringLiteral("HomeLocation"));
0128         home.setLatitude(settings.value(QStringLiteral("Latitude"), NAN).toFloat());
0129         home.setLongitude(settings.value(QStringLiteral("Longitude"), NAN).toFloat());
0130         home.setName(i18n("Home"));
0131         if (home.isValid()) {
0132             beginInsertRows({}, 0, 0);
0133             m_locations.push_back(home);
0134             endInsertRows();
0135             saveLocations();
0136         }
0137         settings.endGroup();
0138         settings.remove(QStringLiteral("HomeLocation"));
0139     }
0140 }
0141 
0142 FavoriteLocationModel::~FavoriteLocationModel() = default;
0143 
0144 void FavoriteLocationModel::appendNewLocation()
0145 {
0146     beginInsertRows({}, rowCount(), rowCount());
0147     FavoriteLocation loc;
0148     switch (rowCount()) {
0149         case 0:
0150             loc.setName(i18n("Home"));
0151             break;
0152         case 1:
0153             loc.setName(i18n("Work"));
0154             break;
0155         default:
0156             loc.setName(i18n("Location %1", rowCount() + 1));
0157             break;
0158     }
0159     m_locations.push_back(loc);
0160     endInsertRows();
0161     saveLocations();
0162 }
0163 
0164 void FavoriteLocationModel::appendLocationIfMissing(FavoriteLocation &&loc)
0165 {
0166     for (const auto &l : m_locations) {
0167         if (KItinerary::LocationUtil::distance(l.latitude(), l.longitude(), loc.latitude(), loc.longitude()) < 10) {
0168             qCDebug(Log) << "Not importing" << loc.name() << "due to" << l.name() << "close by";
0169             return;
0170         }
0171     }
0172     beginInsertRows({}, rowCount(), rowCount());
0173     m_locations.push_back(std::move(loc));
0174     endInsertRows();
0175 }
0176 
0177 void FavoriteLocationModel::removeLocation(int row)
0178 {
0179     beginRemoveRows({}, row, row);
0180     m_locations.erase(m_locations.begin() + row);
0181     endRemoveRows();
0182     saveLocations();
0183 }
0184 
0185 const std::vector<FavoriteLocation>& FavoriteLocationModel::favoriteLocations() const
0186 {
0187     return m_locations;
0188 }
0189 
0190 void FavoriteLocationModel::setFavoriteLocations(std::vector<FavoriteLocation> &&locs)
0191 {
0192     beginResetModel();
0193     m_locations = std::move(locs);
0194     saveLocations();
0195     endResetModel();
0196 }
0197 
0198 int FavoriteLocationModel::rowCount(const QModelIndex &parent) const
0199 {
0200     if (parent.isValid()) {
0201         return 0;
0202     }
0203     return m_locations.size();
0204 }
0205 
0206 QVariant FavoriteLocationModel::data(const QModelIndex &index, int role) const
0207 {
0208     if (!index.isValid()) {
0209         return {};
0210     }
0211 
0212     const auto &loc = m_locations[index.row()];
0213     switch (role) {
0214         case Qt::DisplayRole:
0215             return loc.name();
0216         case FavoriteLocationModel::LatitudeRole:
0217             return loc.latitude();
0218         case FavoriteLocationModel::LongitudeRole:
0219             return loc.longitude();
0220         case FavoriteLocationModel::FavoriteLocationRole:
0221             return QVariant::fromValue(loc);
0222     }
0223 
0224     return {};
0225 }
0226 
0227 bool FavoriteLocationModel::setData(const QModelIndex &index, const QVariant &value, int role)
0228 {
0229     if (!index.isValid()) {
0230         return false;
0231     }
0232 
0233     auto &loc = m_locations[index.row()];
0234     switch (role) {
0235         case Qt::DisplayRole:
0236             loc.setName(value.toString());
0237             Q_EMIT dataChanged(index, index);
0238             saveLocations();
0239             return true;
0240         case FavoriteLocationModel::LatitudeRole:
0241             loc.setLatitude(value.toFloat());
0242             Q_EMIT dataChanged(index, index);
0243             saveLocations();
0244             return true;
0245         case FavoriteLocationModel::LongitudeRole:
0246             loc.setLongitude(value.toFloat());
0247             Q_EMIT dataChanged(index, index);
0248             saveLocations();
0249             return true;
0250     }
0251 
0252     return false;
0253 }
0254 
0255 QHash<int, QByteArray> FavoriteLocationModel::roleNames() const
0256 {
0257     auto r = QAbstractListModel::roleNames();
0258     r.insert(FavoriteLocationModel::LatitudeRole, "latitude");
0259     r.insert(FavoriteLocationModel::LongitudeRole, "longitude");
0260     r.insert(FavoriteLocationModel::FavoriteLocationRole, "favoriteLocation");
0261     return r;
0262 }
0263 
0264 void FavoriteLocationModel::saveLocations() const
0265 {
0266     const auto basePath = favoriteLocationPath();
0267     QDir().mkpath(basePath);
0268     QFile f(basePath + QLatin1StringView("locations.json"));
0269     if (!f.open(QFile::WriteOnly)) {
0270         qWarning() << "Failed to save favorite locations:" << f.errorString() << f.fileName();
0271         return;
0272     }
0273 
0274     f.write(JsonIO::write(FavoriteLocation::toJson(m_locations)));
0275 }
0276 
0277 void FavoriteLocationModel::exportToGpx(const QString &filePath) const
0278 {
0279     if (filePath.isEmpty()) {
0280         return;
0281     }
0282 
0283     QFile f(QUrl(filePath).isLocalFile() ? QUrl(filePath).toLocalFile() : filePath);
0284     if (!f.open(QFile::WriteOnly)) {
0285         qCWarning(Log) << f.errorString() << f.fileName();
0286         return;
0287     }
0288     GpxExport exporter(&f);
0289     for (const auto &fav : m_locations) {
0290         exporter.writeFavoriteLocation(fav);
0291     }
0292 }
0293 
0294 void FavoriteLocationModel::importFromGpx(const QString &filePath)
0295 {
0296     if (filePath.isEmpty()) {
0297         return;
0298     }
0299 
0300     QFile f(QUrl(filePath).isLocalFile() ? QUrl(filePath).toLocalFile() : filePath);
0301     if (!f.open(QFile::ReadOnly)) {
0302         qCWarning(Log) << f.errorString() << f.fileName();
0303         return;
0304     }
0305 
0306     Gpx::Reader reader(&f);
0307     while (reader.readNextStartElement()) {
0308         if (reader.isRootElement()) {
0309             continue;
0310         }
0311         if (reader.isWaypointStart()) {
0312             FavoriteLocation loc;
0313             loc.setLatitude(reader.latitude());
0314             loc.setLongitude(reader.longitude());
0315 
0316             QString name, type;
0317             while (reader.readNext()) {
0318                 if (reader.isWaypointEnd()) {
0319                     break;
0320                 }
0321                 if (reader.isGpxName()) {
0322                     name = reader.gpxName();
0323                 } else if (reader.isGpxType()) {
0324                     type = reader.gpxType();
0325                 }
0326             }
0327 
0328             if (name.isEmpty()) {
0329                 continue;
0330             }
0331             if (!type.isEmpty()) {
0332                 loc.setName(type + QLatin1Char('/') + name);
0333             } else {
0334                 loc.setName(name);
0335             }
0336 
0337             appendLocationIfMissing(std::move(loc));
0338             continue;
0339         }
0340         reader.skipCurrentElement();
0341     }
0342 
0343     saveLocations();
0344 }
0345 
0346 #include "moc_favoritelocationmodel.cpp"