subsurface/core/divesitehelpers.cpp

130 lines
5.4 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0
//
// infrastructure to deal with dive sites
//
#include "divesitehelpers.h"
#include "divesite.h"
#include "errorhelper.h"
#include "subsurface-string.h"
#include "qthelper.h"
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QUrlQuery>
#include <QEventLoop>
#include <QTimer>
#include <QRegularExpression>
#include <memory>
/** Performs a REST get request to a service returning a JSON object. */
static QJsonObject doAsyncRESTGetRequest(const QString& url, int msTimeout)
{
// By making the QNetworkAccessManager static and local to this function,
// only one manager exists for all geo-lookups and it is only initialized
// on first call to this function.
static QNetworkAccessManager rgl;
QNetworkRequest request;
QEventLoop loop;
QTimer timer;
request.setRawHeader("Accept", "text/json");
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
request.setUrl(url);
// By using a std::unique_ptr<>, we can exit from the function at any point
// and the reply will be freed. Likewise, when overwriting the pointer with
// a new value. According to Qt's documentation it is fine the delete the
// reply as long as we're not in a slot connected to error() or finish().
std::unique_ptr<QNetworkReply> reply(rgl.get(request));
timer.setSingleShot(true);
QObject::connect(&*reply, SIGNAL(finished()), &loop, SLOT(quit()));
timer.start(msTimeout);
loop.exec();
if (!timer.isActive()) {
report_error("timeout accessing %s", qPrintable(url));
QObject::disconnect(&*reply, SIGNAL(finished()), &loop, SLOT(quit()));
reply->abort();
return QJsonObject{};
}
timer.stop();
if (reply->error() > 0) {
report_error("got error accessing %s: %s", qPrintable(url), qPrintable(reply->errorString()));
return QJsonObject{};
}
int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (v < 200 || v >= 300) {
return QJsonObject{};
}
QByteArray fullReply = reply->readAll();
QJsonParseError errorObject;
QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject);
if (errorObject.error != QJsonParseError::NoError) {
report_error("error parsing JSON response: %s", qPrintable(errorObject.errorString()));
return QJsonObject{};
}
// Success, return JSON response from server
return jsonDoc.object();
}
/// Performs a reverse-geo-lookup of the coordinates and returns the taxonomy data.
taxonomy_data reverseGeoLookup(degrees_t latitude, degrees_t longitude)
{
const QString geonamesNearbyURL = QStringLiteral("http://api.geonames.org/findNearbyJSON?lang=%1&lat=%2&lng=%3&radius=50&username=dirkhh");
const QString geonamesNearbyPlaceNameURL = QStringLiteral("http://api.geonames.org/findNearbyPlaceNameJSON?lang=%1&lat=%2&lng=%3&radius=50&username=dirkhh");
const QString geonamesOceanURL = QStringLiteral("http://api.geonames.org/oceanJSON?lang=%1&lat=%2&lng=%3&radius=50&username=dirkhh");
QString url;
QJsonObject obj;
taxonomy_data taxonomy = { 0, 0 };
// check the oceans API to figure out the body of water
url = geonamesOceanURL.arg(getUiLanguage().section(QRegularExpression("[-_ ]"), 0, 0)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0);
obj = doAsyncRESTGetRequest(url, 5000); // 5 secs. timeout
QVariantMap oceanName = obj.value("ocean").toVariant().toMap();
if (oceanName["name"].isValid())
taxonomy_set_category(&taxonomy, TC_OCEAN, qPrintable(oceanName["name"].toString()), taxonomy_origin::GEOCODED);
// check the findNearbyPlaces API from geonames - that should give us country, state, city
url = geonamesNearbyPlaceNameURL.arg(getUiLanguage().section(QRegularExpression("[-_ ]"), 0, 0)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0);
obj = doAsyncRESTGetRequest(url, 5000); // 5 secs. timeout
QVariantList geoNames = obj.value("geonames").toVariant().toList();
if (geoNames.count() == 0) {
// check the findNearby API from geonames if the previous search came up empty - that should give us country, state, location
url = geonamesNearbyURL.arg(getUiLanguage().section(QRegularExpression("[-_ ]"), 0, 0)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0);
obj = doAsyncRESTGetRequest(url, 5000); // 5 secs. timeout
geoNames = obj.value("geonames").toVariant().toList();
}
if (geoNames.count() > 0) {
QVariantMap firstData = geoNames.at(0).toMap();
// fill out all the data - start at COUNTRY since we already got OCEAN above
for (int idx = TC_COUNTRY; idx < TC_NR_CATEGORIES; idx++) {
if (firstData[taxonomy_api_names[idx]].isValid()) {
QString value = firstData[taxonomy_api_names[idx]].toString();
taxonomy_set_category(&taxonomy, (taxonomy_category)idx, qPrintable(value), taxonomy_origin::GEOCODED);
}
}
const char *l3 = taxonomy_get_value(&taxonomy, TC_ADMIN_L3);
const char *lt = taxonomy_get_value(&taxonomy, TC_LOCALNAME);
if (empty_string(l3) && !empty_string(lt)) {
// basically this means we did get a local name (what we call town), but just like most places
// we didn't get an adminName_3 - which in some regions is the actual city that town belongs to,
// then we copy the town into the city
taxonomy_set_category(&taxonomy, TC_ADMIN_L3, lt, taxonomy_origin::GEOCOPIED);
}
} else {
report_error("geonames.org did not provide reverse lookup information");
}
return taxonomy;
}