commit 54e240f910489fd5cea90c80be511fda485e61da Author: Tomasz Golinski Date: Sun Sep 8 18:42:10 2019 +0200 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..cbf10cb --- /dev/null +++ b/.clang-format @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# clang-format configuration file. Intended for clang-format >= 4. +# +# For more information, see: +# +# Documentation/process/clang-format.rst +# https://clang.llvm.org/docs/ClangFormat.html +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: true + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +#BreakConstructorInitializers: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +#FixNamespaceComments: false # Unknown to clang-format-4.0 +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: true +#IndentPPDirectives: None # Unknown to clang-format-5.0 +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 8 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true + +# Taken from git's rules +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 10 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 60 + +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Always +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d46065f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.[oa] +*~ +*.backup +*.l[ao] +*.kdev4 +.deps +.libs +.kdev4 +*.kate-swp +pli +/build* +config_user.h diff --git a/Poland.png b/Poland.png new file mode 100644 index 0000000..b1d6db1 Binary files /dev/null and b/Poland.png differ diff --git a/Poland_relief.png b/Poland_relief.png new file mode 100644 index 0000000..25de76a Binary files /dev/null and b/Poland_relief.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4fa32e --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +GEOSTAT +======= + +Tools for computing various statistics related to Geocaching/Opencaching +------------------------------------------------------------------------ + +### Usage + +``` +Usage: [-p] [-g file] [-MDHh] +Generate stats from Opencaching data or GPX files. + + * Data source: + -p use Opencaching.pl + -u user user for Opencaching + -g file use specified gpx file + * Output: + -M print geographically extreme caches + -D print furthest and closest caches + -H file render a heat map to a file + -s n stamp size for a heat map (default = 15) + -L print all caches + -T print D/T matrix + -h display this help screen +``` + +### Installation + +To compile it requires: + +* C++20 compliler (e.g. GCC-9 or Clang-8). +* meson (at least 0.47.0) + ninja build system +* libcurl +* JSON for Modern C++: https://github.com/nlohmann/json +* gpxlib: http://irdvo.nl/gpxlib/ +* ImageMagick or GraphicsMagick Magick++ library + +To build the project run `meson build; cd build; ninja`. You might need to set `CXX` variable to point to a correct compiler. + +### Credits + +Map of Poland `Poland.png` comes from `https://pl.wikipedia.org/wiki/Plik:Poland_location_map.svg` and is licenced under CC-BY-SA and GNU FDL. +Map of Poland `Poland_relief.png` comes from `https://commons.wikimedia.org/wiki/File:Relief_Map_of_Poland.svg` and is licenced under CC-BY-SA. diff --git a/api.h b/api.h new file mode 100644 index 0000000..100997e --- /dev/null +++ b/api.h @@ -0,0 +1,15 @@ +#pragma once + +#include "cache.h" + +#include + +class Api { +public: + Api() {} + virtual ~Api() {} + + // virtual Cache get_cache(std::string code) = 0; + // virtual Caches get_caches(std::vector codes) = 0; + virtual Caches get_user_caches(std::string uuid, int count = 0) = 0; +}; diff --git a/cache.cpp b/cache.cpp new file mode 100644 index 0000000..3b9ac79 --- /dev/null +++ b/cache.cpp @@ -0,0 +1,30 @@ +#include "cache.h" + +#include +const static int Earth_radius = 6378; + +Position Cache::home; + +static float degtorad(float x) { + return x * M_PI / 180; +} +void Cache::show() const { + std::cout << "Cache:\t" << code << ' ' << name << '\n'; + std::cout << '\t' << pos.lat << " " << pos.lon << "\t\t D/T: " << diff << '/' << terr << '\n'; +} + +float Cache::distance() const { + return 2 * Earth_radius * asin(sqrt(pow(sin(degtorad((pos.lat - home.lat) / 2)), 2) + cos(degtorad(pos.lat)) * cos(degtorad(home.lat)) * pow(sin(degtorad((pos.lon - home.lon) / 2)), 2))); +}; + +bool CacheCmpNS(const Cache& lhs, const Cache& rhs) { + return lhs.pos.lat < rhs.pos.lat; +} + +bool CacheCmpEW(const Cache& lhs, const Cache& rhs) { + return lhs.pos.lon < rhs.pos.lon; +} + +bool CacheCmpDist(const Cache& lhs, const Cache& rhs) { + return lhs.distance() < rhs.distance(); +} diff --git a/cache.h b/cache.h new file mode 100644 index 0000000..0018a4d --- /dev/null +++ b/cache.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +enum Service { + gc, + ocpl, + ocde, + ocna, + ocro, + ocuk, + ocnl, + gcsu +}; + +// enum Type { +// traditional, +// multi, +// quiz, // also known as "Mystery" +// moving, // a geocache with changing coordinates +// virt, +// webcam, +// other, // also dubbed "unknown type"; allows OC users to create special caches which don't fit into the scheme of well-known types +// event, // a peculiar type of geocache which is NOT a geocache at all, but it is stored as a geocache in OC database. Just keep in mind, that in case of Event Caches, some fields may have a little different meaning than you would tell by their name +// own, // a moving geocache which is carried by the owner +// podcast //a geocache with attached MP3 file(s). The MP3 data is not accessible via OKAPI. This type is only in use at Opencaching.US +// }; + +class Position { +public: + float lat; + float lon; +}; + +class Cache { +public: + std::string code; + Position pos; + std::string name; + std::string size; + float diff; + float terr; + std::string type; + Service origin; + + static Position home; + + void show() const; + float distance() const; +}; + +class CacheCmp { +public: + bool operator()(const Cache& lhs, const Cache& rhs) const { + return lhs.code < rhs.code; + } +}; + +bool CacheCmpNS(const Cache& lhs, const Cache& rhs); +bool CacheCmpEW(const Cache& lhs, const Cache& rhs); +bool CacheCmpDist(const Cache& lhs, const Cache& rhs); + +typedef std::set Caches; diff --git a/config_user_example.h b/config_user_example.h new file mode 100644 index 0000000..4f88a58 --- /dev/null +++ b/config_user_example.h @@ -0,0 +1,10 @@ +#pragma once + +// Default OCPL user to use when -u is not used +std::string ocpl_user_uuid = ""; + +// Consumer key for OCPL API +std::string ocpl_key = ""; + +// Home location +Cache::home = { 0, 0 }; diff --git a/debug.cpp b/debug.cpp new file mode 100644 index 0000000..5534d80 --- /dev/null +++ b/debug.cpp @@ -0,0 +1,19 @@ +#include "debug.h" + +int Debug::debug_level = 1; + +Debug::Debug(int n) : lvl(n) { + if (lvl <= debug_level) { + std::string indent; + + indent.append(lvl - 1, '\t'); + if (lvl > 1) indent.append("*** "); + std::cout << indent; + } +} + +Debug::~Debug() { + if (lvl <= debug_level) std::cout << '\n'; +} + +void Debug::set_debug_level(int n) { debug_level = n; } diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..6a867a1 --- /dev/null +++ b/debug.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +class Debug { +private: + int lvl; // debug level of the particular instance of Debug stream + static int debug_level; // debug level set globally + +public: + Debug(int n); + ~Debug(); + + // Copy constructor and assignment operators are deleted to prevent extra destructor and constructor calls when chaining << operator + Debug(const Debug&) = delete; + Debug& operator=(const Debug&) = delete; + + template + Debug& operator<<(T const& value) { + if (lvl <= debug_level) + std::cout << value; + return *this; + } + static void set_debug_level(int n); +}; diff --git a/geostat.cpp b/geostat.cpp new file mode 100644 index 0000000..cd63ae2 --- /dev/null +++ b/geostat.cpp @@ -0,0 +1,174 @@ +#include "okapi.h" +#include "gpx.h" +#include "cache.h" +#include "debug.h" +#include "heat.h" + +#include +#include +#include +#include + +int main(int argc, char** argv) { + bool show_minmax = 0; + bool show_dist = 0; + bool show_list = 0; + bool show_dt = 0; + std::string heat_file; + int heat_stamp_size = 15; + bool heat_relief = 0; + bool use_ocpl = 0; + std::string ocpl_user; + std::string ocpl_url = "https://opencaching.pl/okapi/"; + std::string gpx_file; + +#include "config_user.h" + + int o; + while ((o = getopt(argc, argv, "g:d:pu:MDH:s:rLTh")) != -1) + switch (o) { + case 'd': + try { + if (std::stoi(optarg) > 0) { + // Debug(1) << "Setting debug level to " << optarg; + Debug::set_debug_level(std::stoi(optarg)); + } + } + catch (...) { + std::cout << "Option \"-d\" requires a valid number as an argument\n"; + std::exit(EXIT_FAILURE); + } + break; + case 'g': + gpx_file = optarg; + break; + case 'p': + use_ocpl = 1; + break; + case 'u': + ocpl_user = optarg; + break; + case 'M': + show_minmax = 1; + break; + case 'D': + show_dist = 1; + break; + case 'H': + heat_file = optarg; + break; + case 's': + try { + if (std::stoi(optarg) > 0) { + heat_stamp_size = std::stoi(optarg); + } + } + catch (...) { + std::cout << "Option \"-s\" requires a valid number as an argument\n"; + std::exit(EXIT_FAILURE); + } + break; + case 'r': + heat_relief = 1; + break; + case 'L': + show_list = 1; + break; + case 'T': + show_dt = 1; + break; + case 'h': + case '?': + default: + std::cout << "Usage: [-p] [-g file] [-MDHh]\n"; + std::cout << "Generate stats from Opencaching data or GPX files.\n\n"; + std::cout << " * Data source:\n"; + std::cout << "\t-p\tuse Opencaching.pl\n"; + std::cout << "\t-u user\tuser for Opencaching\n"; + std::cout << "\t-g file\tuse specified gpx file\n"; + std::cout << " * Output:\n"; + std::cout << "\t-M\tprint geographically extreme caches\n"; + std::cout << "\t-D\tprint furthest and closest caches\n"; + std::cout << "\t-H file\trender a heat map to a file\n"; + std::cout << "\t-s n\tstamp size for a heat map (default = 15)\n"; + std::cout << "\t-L\tprint all caches\n"; + std::cout << "\t-T\tprint D/T matrix\n"; + std::cout << "\t-h\tdisplay this help screen\n"; + std::exit(EXIT_FAILURE); + } + + Caches cc; + + if (use_ocpl) { + Okapi OCpl(ocpl_url, ocpl_key); + if (!ocpl_user.empty()) { + ocpl_user_uuid = OCpl.get_uuid(ocpl_user); + } + cc.merge(OCpl.get_user_caches(ocpl_user_uuid, 0)); + } + + if (!gpx_file.empty()) { + GPX gpxfile(gpx_file); + cc.merge(gpxfile.get_user_caches()); + } + + // TODO: some cache deduplication is needed + + Debug(2) << "Caches read: " << cc.size() << '\n'; + + if (!heat_file.empty()) { + Heat hmap(&cc); + hmap.generate(heat_file, heat_stamp_size, heat_relief); + } + + if (show_minmax) { + auto N = std::max_element(cc.begin(), cc.end(), CacheCmpNS); + auto S = std::min_element(cc.begin(), cc.end(), CacheCmpNS); + auto E = std::max_element(cc.begin(), cc.end(), CacheCmpEW); + auto W = std::min_element(cc.begin(), cc.end(), CacheCmpEW); + + std::cout << "Most N:\n"; + N->show(); + std::cout << "Most S:\n"; + S->show(); + std::cout << "Most E:\n"; + E->show(); + std::cout << "Most W:\n"; + W->show(); + } + + if (show_dist) { + auto far = std::max_element(cc.begin(), cc.end(), CacheCmpDist); + auto near = std::min_element(cc.begin(), cc.end(), CacheCmpDist); + + std::cout << "Nearest cache: " << near->distance() << " km\n"; + near->show(); + std::cout << "Furthest cache: " << far->distance() << " km\n"; + far->show(); + std::cout << "Dist:\t" << far->distance() << " km\n"; + } + + if (show_list) { + for (auto el : cc) + el.show(); + } + + if (show_dt) { + short dt_table[11][11]; + + std::cout << std::setw(5) << "D\\T"; + for (int j = 2; j <= 10; j++) { // print table terr headers + std::cout << std::setw(5) << j / 2.0; + } + std::cout << '\n'; + + for (int i = 2; i <= 10; i++) { // i -> diff in rows + std::cout << std::setw(5) << i / 2.0; + for (int j = 2; j <= 10; j++) { // j -> terr in cols + dt_table[i][j] = std::count_if(cc.begin(), cc.end(), [i, j](Cache c) { return (c.diff == i / 2.0 && c.terr == j / 2.0); }); + std::cout << std::setw(5) << dt_table[i][j]; + } + std::cout << '\n'; + } + } +} diff --git a/gpx.cpp b/gpx.cpp new file mode 100644 index 0000000..d393a8c --- /dev/null +++ b/gpx.cpp @@ -0,0 +1,76 @@ +#include "gpx.h" +#include "cache.h" +#include "debug.h" + +#include +#include +#include +#include +#include "gpx/Parser.h" +#include "gpx/Report.h" + +class ReportDebug : public gpx::Report { +public: + ReportDebug() {} + ~ReportDebug() {} + void report(const gpx::Node* node, gpx::Report::Warning warning, const std::string& extra) { + std::string msg; + + if (node != nullptr) { + msg += (node->getType() == gpx::Node::ATTRIBUTE ? "Attribute " : "Element ") + node->getName() + " : "; + } + + msg += gpx::Report::text(warning); + + if (!extra.empty()) { + msg += ": " + extra; + } + + Debug(3) << msg + ".\n"; + } +}; + +GPX::GPX(std::string path) { + std::ifstream stream(path); + if (stream.is_open()) { + ReportDebug report; + gpx::Parser parser(&report); + root = parser.parse(stream); + + if (root == nullptr) { + Debug(1) << "Parsing of file " << path << " failed due to " << parser.errorText() << " on line " << parser.errorLineNumber() << " and column " << parser.errorColumnNumber() << '\n'; + } else { + items = root->wpts().list(); + } + } +} + +GPX::~GPX() { + delete root; +} + +Caches GPX::get_user_caches(std::string uuid, int count) { + Caches list; + + for (auto& el : items) { + if (el->sym().getValue() == "Geocache Found") { + Cache c; + c.code = el->name().getValue(); + c.name = el->desc().getValue(); + c.pos.lat = stof(el->lat().getValue()); + c.pos.lon = stof(el->lon().getValue()); + + if (c.code.starts_with("GC")) + c.origin = gc; + else if (c.code.starts_with("OP")) + c.origin = ocpl; + else if (c.code.starts_with("TR") || c.code.starts_with("VI") || c.code.starts_with("MV")) + c.origin = gcsu; + + list.insert(c); + } + } + Debug(2) << "Caches read from GPX file: " << list.size() << '\n'; + + return list; +} diff --git a/gpx.h b/gpx.h new file mode 100644 index 0000000..94b53ff --- /dev/null +++ b/gpx.h @@ -0,0 +1,24 @@ +#pragma once + +#include "api.h" + +#include +#include + +namespace gpx { +class WPT; +class GPX; +} // namespace gpx + +class GPX : Api { +private: + std::list items; + gpx::GPX* root; + +public: + GPX(std::string path); + ~GPX(); + + Cache get_cache(std::string code); + Caches get_user_caches(std::string uuid = "", int count = 0); +}; diff --git a/heat.cpp b/heat.cpp new file mode 100644 index 0000000..2c22768 --- /dev/null +++ b/heat.cpp @@ -0,0 +1,43 @@ +#include "heat.h" +#include "cache.h" + +#include +#include +#include + +#include +#include +#include + +Heat::Heat(Caches* cc) : points(cc) { +#ifdef graphicsmagick + Magick::InitializeMagick(nullptr); +#endif +} + +void Heat::generate(std::string filename, int size, bool relief, std::string theme) { + heatmap_t* hm = heatmap_new(size_x, size_y); + heatmap_stamp_t* stamp = heatmap_stamp_gen(size); + std::vector image(size_x * size_y * 4); + + for (auto el : *points) { + if (el.pos.lon >= lon_min && el.pos.lon <= lon_max && el.pos.lat >= lat_min && el.pos.lat <= lat_max) + heatmap_add_point_with_stamp(hm, (el.pos.lon - lon_min) / (lon_max - lon_min) * size_x, size_y - static_cast((el.pos.lat - lat_min) / (lat_max - lat_min) * size_y), stamp); + // Debug(2) << static_cast((el.pos.lon - lon_min) / (lon_max - lon_min) * size_x) << " " << size_y - static_cast((el.pos.lat - lat_min) / (lat_max - lat_min) * size_y) << "\n"; + } + + heatmap_render_to(hm, heatmap_cs_Spectral_soft, &image[0]); + heatmap_free(hm); + + Magick::Image contour; + if (relief) + contour.read(relief_file); + else + contour.read(contour_file); + + Magick::Image heatmap(size_x, size_y, "RGBA", Magick::CharPixel, &image[0]); + + contour.composite(heatmap, 0, 0, Magick::OverCompositeOp); + contour.write(filename); + // heatmap.write("geostat_heat.png"); +} diff --git a/heat.h b/heat.h new file mode 100644 index 0000000..0b091b7 --- /dev/null +++ b/heat.h @@ -0,0 +1,25 @@ +#pragma once + +#include "cache.h" + +#include +#include + +class Heat { +private: + Caches* points; + const int size_x = 1000; + const int size_y = 972; + const float lat_max = 55.2; + const float lat_min = 48.7; + const float lon_max = 24.5; + const float lon_min = 13.8; + + const std::string contour_file = "Poland.png"; + const std::string relief_file = "Poland_relief.png"; + +public: + Heat(Caches* cc); + + void generate(std::string filename, int size, bool relief, std::string theme = ""); +}; diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..aa204fa --- /dev/null +++ b/meson.build @@ -0,0 +1,22 @@ +project('geostat', 'cpp', default_options : ['cpp_std=c++2a']) +curl_dep = dependency('libcurl') +json_dep = dependency('nlohmann_json') +#tinyxml_dep = dependency('tinyxml2') + +magick_dep = dependency('Magick++', required : false) +if not magick_dep.found() + magick_dep = dependency('GraphicsMagick++', required : false) +endif +if not magick_dep.found() + error('ImageMagick++ or GraphicsMagick++ not found.') +else + add_global_arguments('-Dgraphicsmagick', language : 'cpp') +endif + +link = ['-lgpx', '-lheatmap'] +src = ['geostat.cpp', 'okapi.cpp', 'gpx.cpp', 'cache.cpp', 'debug.cpp', 'heat.cpp'] + +executable('geostat', src, dependencies : [curl_dep, json_dep, magick_dep], link_args: link) + +configure_file(input: 'Poland.png', output: 'Poland.png', copy: true) +configure_file(input: 'Poland_relief.png', output: 'Poland_relief.png', copy: true) diff --git a/okapi.cpp b/okapi.cpp new file mode 100644 index 0000000..3957634 --- /dev/null +++ b/okapi.cpp @@ -0,0 +1,182 @@ +#include "okapi.h" +#include "cache.h" +#include "debug.h" + +#include +#include + +#include +#include + +using json = nlohmann::json; + +static const std::string OKAPI_logs = "services/logs/userlogs"; +static const std::string OKAPI_cache = "services/caches/geocache"; +static const std::string OKAPI_caches = "services/caches/geocaches"; +static const std::string OKAPI_username = "services/users/by_username"; + +size_t Okapi::write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) { + std::string* str = reinterpret_cast(userdata); + str->append(ptr, nmemb); + return nmemb; +} + +std::string Okapi::curl_post(std::string url, std::string post) { + CURL* curl; + CURLcode res; + + std::string output; + + Debug(5) << "API query: " << post; + + curl = curl_easy_init(); + if (!curl) { + curl_global_cleanup(); + throw 0; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&output); + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + Debug(1) << "curl_easy_perform() failed: " << curl_easy_strerror(res) << '\n'; + curl_easy_cleanup(curl); + std::exit(EXIT_FAILURE); + } + curl_easy_cleanup(curl); + + Debug(5) << "API query result: " << output; + if (output.starts_with("{\"error\"")) { + json j = json::parse(output); + Debug(1) << "OKAPI error: " << j["error"]["developer_message"]; + std::exit(EXIT_FAILURE); + } + return output; +} + +std::string Okapi::get_user_caches_json(std::string uuid, int count, int offset) { + std::string service = url + OKAPI_logs; + std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=cache_code|type&limit=" + std::to_string(count) + "&offset=" + std::to_string(offset); + return curl_post(service, query); +} + +// std::string Okapi::get_cache_json(std::string code) { +// std::string service = url + OKAPI_cache; +// std::string query = "consumer_key=" + key + "&cache_code=" + code + "&fields=code|name|location|type|status|difficulty|terrain"; +// return curl_post(service, query); +// } + +std::string Okapi::get_caches_json(std::string codes) { + std::string service = url + OKAPI_caches; + std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|name|location|type|status|difficulty|terrain"; + return curl_post(service, query); +} + +// Cache Okapi::get_cache(std::string code) { +// std::string output = get_cache_json(code); +// json j = json::parse(output); +// +// Cache c; +// c.code = code; +// c.name = j["name"]; +// c.type = j["type"]; +// c.diff = j["difficulty"].get(); +// c.terr = j["terrain"].get(); +// // std::cout << j["difficulty"] << '\n'; +// // std::cout << j["difficulty"].get() << '\n'; +// // std::cout << j["difficulty"].get() << '\n'; +// // std::cout << j["difficulty"].get() << '\n'; +// +// std::string loc = j["location"]; +// int pos = loc.find("|"); +// c.pos.lat = stof(loc.substr(0, pos)); +// c.pos.lon = stof(loc.substr(pos + 1)); +// +// return c; +// } + +Caches Okapi::get_caches(std::vector codes) { + Caches cc; + Cache c; + + while (codes.size() > 0) { + std::string codes_list; + int n = (codes.size() > 500) ? 500 : codes.size(); + for (int i = 0; i < n; i++) { + codes_list += codes.back(); + codes_list += '|'; + codes.pop_back(); + } + codes_list.pop_back(); // remove trailing '|' + + std::string output = get_caches_json(codes_list); + json j = json::parse(output); + + for (auto& el : j.items()) { + c.code = el.value()["code"]; + c.name = el.value()["name"]; + c.type = el.value()["type"]; + c.diff = el.value()["difficulty"]; + c.terr = el.value()["terrain"]; + ; + std::string loc = el.value()["location"]; + int pos = loc.find("|"); + c.pos.lat = stof(loc.substr(0, pos)); + c.pos.lon = stof(loc.substr(pos + 1)); + c.origin = ocpl; + cc.insert(c); + } + } + return cc; +} + +Caches Okapi::get_user_caches(std::string uuid, int count) { + Caches cc; + std::vector codes; + json j; + + int off = 0; + + if (count == 0) + do { + std::string output = get_user_caches_json(uuid, 1000, off); + j = json::parse(output); + + for (auto& el : j.items()) { + if (el.value()["type"] == "Found it") { + codes.emplace_back(el.value()["cache_code"]); + } + } + off += j.size(); + } while (j.size() > 0); + else { + int count_req = (count > 1000) ? 1000 : count; + do { + std::string output = get_user_caches_json(uuid, count_req, off); + j = json::parse(output); + + for (auto& el : j.items()) { + if (el.value()["type"] == "Found it") { + codes.emplace_back(el.value()["cache_code"]); + } + } + off += j.size(); + count -= count_req; + } while (j.size() > 0 && count > 0); + } + + // Debug(3) << codes; + cc = get_caches(codes); + Debug(2) << "Caches read from OC: " << cc.size() << '\n'; + return cc; +} + +std::string Okapi::get_uuid(std::string username) { + std::string service = url + OKAPI_username; + std::string query = "consumer_key=" + key + "&username=" + username + "&fields=uuid"; + json j = json::parse(curl_post(service, query)); + return j["uuid"]; +} diff --git a/okapi.h b/okapi.h new file mode 100644 index 0000000..fcd1010 --- /dev/null +++ b/okapi.h @@ -0,0 +1,26 @@ +#pragma once + +#include "api.h" + +#include + +class Okapi : Api { +private: + std::string url; + std::string key; + + static size_t write_cb(char* ptr, size_t size, size_t nmemb, void* userdata); + std::string curl_post(std::string url, std::string post); + std::string get_user_caches_json(std::string uuid, int count = 0, int offset = 0); + // std::string get_cache_json(std::string code); + std::string get_caches_json(std::string codes); + +public: + Okapi(std::string server_url, std::string consumer_key) : url(server_url), key(consumer_key) {} + + // Cache get_cache(std::string code); + Caches get_caches(std::vector codes); + Caches get_user_caches(std::string uuid, int count = 0); + + std::string get_uuid(std::string username); +};