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"