tdesktop/Telegram/SourceFiles/core/current_geo_location.cpp
2024-07-19 11:20:54 +02:00

243 lines
6.6 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/current_geo_location.h"
#include "base/platform/base_platform_info.h"
#include "base/invoke_queued.h"
#include "base/timer.h"
#include "data/raw/raw_countries_bounds.h"
#include "platform/platform_current_geo_location.h"
#include "ui/ui_utility.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
namespace Core {
namespace {
constexpr auto kDestroyManagerTimeout = 20 * crl::time(1000);
[[nodiscard]] QString ChooseLanguage(const QString &language) {
// https://docs.mapbox.com/api/search/geocoding#language-coverage
auto result = language.toLower().replace('-', '_');
static const auto kGood = std::array{
// Global coverage.
u"de"_q, u"en"_q, u"es"_q, u"fr"_q, u"it"_q, u"nl"_q, u"pl"_q,
// Local coverage.
u"az"_q, u"bn"_q, u"ca"_q, u"cs"_q, u"da"_q, u"el"_q, u"fa"_q,
u"fi"_q, u"ga"_q, u"hu"_q, u"id"_q, u"is"_q, u"ja"_q, u"ka"_q,
u"km"_q, u"ko"_q, u"lt"_q, u"lv"_q, u"mn"_q, u"pt"_q, u"ro"_q,
u"sk"_q, u"sq"_q, u"sv"_q, u"th"_q, u"tl"_q, u"uk"_q, u"vi"_q,
u"zh"_q, u"zh_Hans"_q, u"zh_TW"_q,
// Limited coverage.
u"ar"_q, u"bs"_q, u"gu"_q, u"he"_q, u"hi"_q, u"kk"_q, u"lo"_q,
u"my"_q, u"nb"_q, u"ru"_q, u"sr"_q, u"te"_q, u"tk"_q, u"tr"_q,
u"zh_Hant"_q,
};
for (const auto &known : kGood) {
if (known.toLower() == result) {
return known;
}
}
if (const auto delimeter = result.indexOf('_'); delimeter > 0) {
result = result.mid(0, delimeter);
for (const auto &known : kGood) {
if (known == result) {
return known;
}
}
}
return u"en"_q;
}
void ResolveLocationAddressGeneric(
const GeoLocation &location,
const QString &language,
const QString &token,
Fn<void(GeoAddress)> callback) {
const auto partialUrl = u"https://api.mapbox.com/search/geocode/v6"
"/reverse?longitude=%1&latitude=%2&language=%3&access_token=%4"_q
.arg(location.point.y())
.arg(location.point.x())
.arg(ChooseLanguage(language));
static auto Cache = base::flat_map<QString, GeoAddress>();
const auto i = Cache.find(partialUrl);
if (i != end(Cache)) {
callback(i->second);
return;
}
const auto finishWith = [=](GeoAddress result) {
Cache[partialUrl] = result;
callback(result);
};
struct State final : QObject {
explicit State(QObject *parent)
: QObject(parent)
, manager(this)
, destroyer([=] { if (sent.empty()) delete this; }) {
}
QNetworkAccessManager manager;
std::vector<QPointer<QNetworkReply>> sent;
base::Timer destroyer;
};
static auto state = QPointer<State>();
if (!state) {
state = Ui::CreateChild<State>(qApp);
}
const auto destroyReplyDelayed = [](QNetworkReply *reply) {
InvokeQueued(reply, [=] {
for (auto i = begin(state->sent); i != end(state->sent);) {
if (!*i || *i == reply) {
i = state->sent.erase(i);
} else {
++i;
}
}
delete reply;
if (state->sent.empty()) {
state->destroyer.callOnce(kDestroyManagerTimeout);
}
});
};
auto request = QNetworkRequest(partialUrl.arg(token));
request.setRawHeader("Referer", "http://desktop-app-resource/");
const auto reply = state->manager.get(request);
QObject::connect(reply, &QNetworkReply::finished, [=] {
destroyReplyDelayed(reply);
const auto json = QJsonDocument::fromJson(reply->readAll());
if (!json.isObject()) {
finishWith({});
return;
}
const auto features = json["features"].toArray();
if (features.isEmpty()) {
finishWith({});
return;
}
const auto feature = features.at(0).toObject();
const auto properties = feature["properties"].toObject();
const auto context = properties["context"].toObject();
auto names = QStringList();
auto add = [&](std::vector<QString> keys) {
for (const auto &key : keys) {
const auto value = context[key];
if (value.isObject()) {
const auto name = value.toObject()["name"].toString();
if (!name.isEmpty()) {
names.push_back(name);
break;
}
}
}
};
add({ /*u"address"_q, u"street"_q, */u"neighborhood"_q });
add({ u"place"_q, u"region"_q });
add({ u"country"_q });
finishWith({ .name = names.join(", ") });
});
QObject::connect(reply, &QNetworkReply::errorOccurred, [=] {
destroyReplyDelayed(reply);
finishWith({});
});
}
} // namespace
GeoLocation ResolveCurrentCountryLocation() {
const auto iso2 = Platform::SystemCountry().toUpper();
const auto &bounds = Raw::CountryBounds();
const auto i = bounds.find(iso2);
if (i == end(bounds)) {
return {
.accuracy = GeoLocationAccuracy::Failed,
};
}
return {
.point = {
(i->second.minLat + i->second.maxLat) / 2.,
(i->second.minLon + i->second.maxLon) / 2.,
},
.bounds = {
i->second.minLat,
i->second.minLon,
i->second.maxLat - i->second.minLat,
i->second.maxLon - i->second.minLon,
},
.accuracy = GeoLocationAccuracy::Country,
};
}
void ResolveCurrentGeoLocation(Fn<void(GeoLocation)> callback) {
using namespace Platform;
return ResolveCurrentExactLocation([done = std::move(callback)](
GeoLocation result) {
done(result.accuracy != GeoLocationAccuracy::Failed
? result
: ResolveCurrentCountryLocation());
});
}
void ResolveLocationAddress(
const GeoLocation &location,
const QString &language,
const QString &token,
Fn<void(GeoAddress)> callback) {
auto done = [=, done = std::move(callback)](GeoAddress result) mutable {
if (!result && !token.isEmpty()) {
ResolveLocationAddressGeneric(
location,
language,
token,
std::move(done));
} else {
done(result);
}
};
Platform::ResolveLocationAddress(location, language, std::move(done));
}
bool AreTheSame(const GeoLocation &a, const GeoLocation &b) {
if (a.accuracy != GeoLocationAccuracy::Exact
|| b.accuracy != GeoLocationAccuracy::Exact) {
return false;
}
const auto normalize = [](float64 value) {
value = std::fmod(value + 180., 360.);
return (value + (value < 0. ? 360. : 0.)) - 180.;
};
constexpr auto kEpsilon = 0.0001;
const auto lon1 = normalize(a.point.y());
const auto lon2 = normalize(b.point.y());
const auto diffLat = std::abs(a.point.x() - b.point.x());
if (std::abs(a.point.x()) >= (90. - kEpsilon)
|| std::abs(b.point.x()) >= (90. - kEpsilon)) {
return diffLat <= kEpsilon;
}
auto diffLon = std::abs(lon1 - lon2);
if (diffLon > 180.) {
diffLon = 360. - diffLon;
}
return diffLat <= kEpsilon && diffLon <= kEpsilon;
}
} // namespace Core