diff options
Diffstat (limited to 'src/apps/projsync.cpp')
| -rw-r--r-- | src/apps/projsync.cpp | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/src/apps/projsync.cpp b/src/apps/projsync.cpp new file mode 100644 index 00000000..3fab38e1 --- /dev/null +++ b/src/apps/projsync.cpp @@ -0,0 +1,555 @@ +/****************************************************************************** + * Project: PROJ + * Purpose: Downloader tool + * Author: Even Rouault, <even.rouault at spatialys.com> + * + ****************************************************************************** + * Copyright (c) 2020, Even Rouault, <even.rouault at spatialys.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +//! @cond Doxygen_Suppress + +#define FROM_PROJ_CPP + +#include <cstdlib> +#include <iostream> +#include <limits> +#include <set> +#include <string> + +#include "filemanager.hpp" +#include "proj.h" +#include "proj_internal.h" + +#include "proj/internal/include_nlohmann_json.hpp" +#include "proj/internal/internal.hpp" + +using json = nlohmann::json; +using namespace NS_PROJ::internal; + +// --------------------------------------------------------------------------- + +namespace { +class ParsingException : public std::exception { + std::string msg_; + + public: + ParsingException(const char *msg) : msg_(msg) {} + const char *what() const noexcept override { return msg_.c_str(); } +}; +} + +// --------------------------------------------------------------------------- + +static void usage() { + std::cerr << "usage: projsync " << std::endl; + std::cerr << " [--endpoint URL]" << std::endl; + std::cerr << " [--local-geojson-file FILENAME]" << std::endl; + std::cerr << " ([--user-writable-directory] | " + "[--system-directory] | [--target-dir DIRNAME])" + << std::endl; + std::cerr << " [--bbox west_long,south_lat,east_long,north_lat]" + << std::endl; + std::cerr << " [--spatial-test contains|intersects]" << std::endl; + std::cerr << " [--source-id ID] [--area-of-use NAME]" << std::endl; + std::cerr << " [--file NAME]" << std::endl; + std::cerr << " [--all] [--exclude-world-coverage]" << std::endl; + std::cerr << " [--quiet] [--dry-run] [--list-files]" << std::endl; + std::exit(1); +} + +// --------------------------------------------------------------------------- + +static std::vector<double> get_bbox(const json &j) { + std::vector<double> res; + if (j.size() == 2 && j[0].is_number() && j[1].is_number()) { + res.push_back(j[0].get<double>()); + res.push_back(j[1].get<double>()); + res.push_back(j[0].get<double>()); + res.push_back(j[1].get<double>()); + } else { + for (const auto &obj : j) { + if (obj.is_array()) { + const auto subres = get_bbox(obj); + if (subres.size() == 4) { + if (res.empty()) { + res = subres; + } else { + res[0] = std::min(res[0], subres[0]); + res[1] = std::min(res[1], subres[1]); + res[2] = std::max(res[2], subres[2]); + res[3] = std::max(res[3], subres[3]); + } + } + } + } + } + return res; +} + +// --------------------------------------------------------------------------- + +int main(int argc, char *argv[]) { + auto ctx = pj_get_default_ctx(); + + std::string targetDir; + std::string endpoint(pj_context_get_url_endpoint(ctx)); + const std::string geojsonFile("files.geojson"); + std::string queriedSourceId; + std::string queriedAreaOfUse; + bool listFiles = false; + bool dryRun = false; + bool hasQueriedBbox = false; + double queried_west = 0.0; + double queried_south = 0.0; + double queried_east = 0.0; + double queried_north = 0.0; + bool intersects = true; + bool quiet = false; + bool includeWorldCoverage = true; + bool queryAll = false; + std::string queriedFilename; + std::string files_geojson_local; + + for (int i = 1; i < argc; i++) { + std::string arg(argv[i]); + if (arg == "--endpoint" && i + 1 < argc) { + i++; + endpoint = argv[i]; + } else if (arg == "--user-writable-directory") { + // do nothing + } else if (arg == "--system-directory") { + targetDir = PROJ_LIB; + } else if (arg == "--target-dir" && i + 1 < argc) { + i++; + targetDir = argv[i]; + } else if (arg == "--local-geojson-file" && i + 1 < argc) { + i++; + files_geojson_local = argv[i]; + } else if (arg == "--list-files") { + listFiles = true; + } else if (arg == "--source-id" && i + 1 < argc) { + i++; + queriedSourceId = argv[i]; + } else if (arg == "--area-of-use" && i + 1 < argc) { + i++; + queriedAreaOfUse = argv[i]; + } else if (arg == "--file" && i + 1 < argc) { + i++; + queriedFilename = argv[i]; + } else if (arg == "--bbox" && i + 1 < argc) { + i++; + auto bboxStr(argv[i]); + auto bbox(split(bboxStr, ',')); + if (bbox.size() != 4) { + std::cerr << "Incorrect number of values for option --bbox: " + << bboxStr << std::endl; + usage(); + } + try { + queried_west = c_locale_stod(bbox[0]); + queried_south = c_locale_stod(bbox[1]); + queried_east = c_locale_stod(bbox[2]); + queried_north = c_locale_stod(bbox[3]); + } catch (const std::exception &e) { + std::cerr << "Invalid value for option --bbox: " << bboxStr + << ", " << e.what() << std::endl; + usage(); + } + if (queried_west > 180 && queried_east > queried_west) { + queried_west -= 360; + queried_east -= 360; + } else if (queried_west < -180 && queried_east > queried_west) { + queried_west += 360; + queried_east += 360; + } else if (fabs(queried_west) < 180 && fabs(queried_east) < 180 && + queried_east < queried_west) { + queried_east += 360; + } + hasQueriedBbox = true; + } else if (arg == "--spatial-test" && i + 1 < argc) { + i++; + const std::string value(argv[i]); + if (ci_equal(value, "contains")) { + intersects = false; + } else if (ci_equal(value, "intersects")) { + intersects = true; + } else { + std::cerr << "Unrecognized value for option --spatial-test: " + << value << std::endl; + usage(); + } + } else if (arg == "--dry-run") { + dryRun = true; + } else if (arg == "--exclude-world-coverage") { + includeWorldCoverage = false; + } else if (arg == "--all") { + queryAll = true; + } else if (arg == "-q" || arg == "--quiet") { + quiet = true; + } else { + usage(); + } + } + if (!listFiles && queriedFilename.empty() && queriedSourceId.empty() && + queriedAreaOfUse.empty() && !hasQueriedBbox && !queryAll) { + std::cerr << "At least one of --list-files, --file, --source-id, " + "--area-of-use, --bbox or --all must be specified." + << std::endl + << std::endl; + usage(); + } + + if (targetDir.empty()) { + targetDir = pj_context_get_user_writable_directory(ctx, true); + } else { + if (targetDir.back() == '/') { + targetDir.resize(targetDir.size() - 1); + } + + // This is used by projsync() to determine where to write files. + pj_context_set_user_writable_directory(ctx, targetDir); + } + + if (!endpoint.empty() && endpoint.back() == '/') { + endpoint.resize(endpoint.size() - 1); + } + + if (!quiet) { + std::cout << "Downloading from " << endpoint << " into " << targetDir + << std::endl; + } + + proj_context_set_enable_network(ctx, true); + if (files_geojson_local.empty()) { + const std::string files_geojson_url(endpoint + '/' + geojsonFile); + if (!proj_download_file(ctx, files_geojson_url.c_str(), false, nullptr, + nullptr)) { + std::cerr << "Cannot download " << geojsonFile << std::endl; + std::exit(1); + } + + files_geojson_local = targetDir + '/' + geojsonFile; + } + + auto file = NS_PROJ::FileManager::open(ctx, files_geojson_local.c_str(), + NS_PROJ::FileAccess::READ_ONLY); + if (!file) { + std::cerr << "Cannot open " << files_geojson_local << std::endl; + std::exit(1); + } + + std::string text; + while (true) { + bool maxLenReached = false; + bool eofReached = false; + text += file->read_line(1000000, maxLenReached, eofReached); + if (maxLenReached) { + std::cerr << "Error while parsing " << geojsonFile + << " : too long line" << std::endl; + std::exit(1); + } + if (eofReached) + break; + } + file.reset(); + + if (listFiles) { + std::cout << "filename,source_id,area_of_use,file_size" << std::endl; + } + + try { + const auto j = json::parse(text); + bool foundMatchSourceIdCriterion = false; + std::set<std::string> source_ids; + bool foundMatchAreaOfUseCriterion = false; + std::set<std::string> areas_of_use; + bool foundMatchFileCriterion = false; + std::set<std::string> files; + if (!j.is_object() || !j.contains("features")) { + throw ParsingException("no features member"); + } + std::vector<std::string> to_download; + unsigned long long total_size_to_download = 0; + for (const auto feat : j["features"]) { + if (!feat.is_object()) { + continue; + } + if (!feat.contains("properties")) { + continue; + } + const auto properties = feat["properties"]; + if (!properties.is_object()) { + continue; + } + + if (!properties.contains("name")) { + continue; + } + const auto j_name = properties["name"]; + if (!j_name.is_string()) { + continue; + } + const auto name(j_name.get<std::string>()); + files.insert(name); + + if (!properties.contains("source_id")) { + continue; + } + const auto j_source_id = properties["source_id"]; + if (!j_source_id.is_string()) { + continue; + } + const auto source_id(j_source_id.get<std::string>()); + source_ids.insert(source_id); + + std::string area_of_use; + if (properties.contains("area_of_use")) { + const auto j_area_of_use = properties["area_of_use"]; + if (j_area_of_use.is_string()) { + area_of_use = j_area_of_use.get<std::string>(); + areas_of_use.insert(area_of_use); + } + } + + unsigned long long file_size = 0; + if (properties.contains("file_size")) { + const auto j_file_size = properties["file_size"]; + if (j_file_size.type() == json::value_t::number_unsigned) { + file_size = j_file_size.get<unsigned long long>(); + } + } + + if (listFiles) { + std::cout << name << "," << area_of_use << "," << source_id + << "," << file_size << std::endl; + continue; + } + + const bool matchSourceId = + queryAll || queriedSourceId.empty() || + source_id.find(queriedSourceId) != std::string::npos; + if (!queriedSourceId.empty() && + source_id.find(queriedSourceId) != std::string::npos) { + foundMatchSourceIdCriterion = true; + } + + const bool matchAreaOfUse = + queryAll || queriedAreaOfUse.empty() || + area_of_use.find(queriedAreaOfUse) != std::string::npos; + if (!queriedAreaOfUse.empty() && + area_of_use.find(queriedAreaOfUse) != std::string::npos) { + foundMatchAreaOfUseCriterion = true; + } + + const bool matchFile = + queryAll || queriedFilename.empty() || + name.find(queriedFilename) != std::string::npos; + if (!queriedFilename.empty() && + name.find(queriedFilename) != std::string::npos) { + foundMatchFileCriterion = true; + } + + bool matchBbox = true; + if (queryAll || hasQueriedBbox) { + matchBbox = false; + do { + if (!feat.contains("geometry")) { + if (queryAll) { + matchBbox = true; + } + break; + } + const auto j_geometry = feat["geometry"]; + if (!j_geometry.is_object()) { + if (queryAll) { + matchBbox = true; + } + break; + } + if (!j_geometry.contains("coordinates")) { + break; + } + const auto j_coordinates = j_geometry["coordinates"]; + if (!j_coordinates.is_array()) { + break; + } + if (!j_geometry.contains("type")) { + break; + } + const auto j_geometry_type = j_geometry["type"]; + if (!j_geometry_type.is_string()) { + break; + } + const auto geometry_type( + j_geometry_type.get<std::string>()); + std::vector<double> grid_bbox; + if (geometry_type == "MultiPolygon") { + std::vector<std::vector<double>> grid_bboxes; + bool foundMinus180 = false; + bool foundPlus180 = false; + for (const auto obj : j_coordinates) { + if (obj.is_array()) { + const auto tmp = get_bbox(obj); + if (tmp.size() == 4) { + if (tmp[0] == -180) + foundMinus180 = true; + else if (tmp[2] == 180) + foundPlus180 = true; + grid_bboxes.push_back(tmp); + } + } + } + for (auto &bbox : grid_bboxes) { + if (foundMinus180 && foundPlus180 && + bbox[0] == -180) { + bbox[0] = 180; + bbox[2] += 360; + } + if (grid_bbox.empty()) { + grid_bbox = bbox; + } else { + grid_bbox[0] = std::min(grid_bbox[0], bbox[0]); + grid_bbox[1] = std::min(grid_bbox[1], bbox[1]); + grid_bbox[2] = std::max(grid_bbox[2], bbox[2]); + grid_bbox[3] = std::max(grid_bbox[3], bbox[3]); + } + } + } else { + grid_bbox = get_bbox(j_coordinates); + } + if (grid_bbox.size() != 4) { + break; + } + double grid_w = grid_bbox[0]; + const double grid_s = grid_bbox[1]; + double grid_e = grid_bbox[2]; + const double grid_n = grid_bbox[3]; + if (grid_e - grid_w > 359 && grid_n - grid_s > 179) { + if (!includeWorldCoverage) { + break; + } + grid_w = -std::numeric_limits<double>::max(); + grid_e = std::numeric_limits<double>::max(); + } else if (grid_e > 180 && queried_west < -180) { + grid_w -= 360; + grid_e -= 360; + } + if (queryAll) { + matchBbox = true; + break; + } + + if (intersects) { + if (queried_west < grid_e && grid_w < queried_east && + queried_south < grid_n && grid_s < queried_north) { + matchBbox = true; + } + } else { + if (grid_w >= queried_west && grid_s >= queried_south && + grid_e <= queried_east && grid_n <= queried_north) { + matchBbox = true; + } + } + + } while (false); + } + + if (matchFile & matchSourceId && matchAreaOfUse && matchBbox) { + const std::string resource_url(endpoint + '/' + name); + if (proj_is_download_needed(ctx, resource_url.c_str(), false)) { + total_size_to_download += file_size; + to_download.push_back(resource_url); + } else { + if (!quiet) { + std::cout << resource_url << " already downloaded." + << std::endl; + } + } + } + } + + if (!quiet && total_size_to_download > 0) { + if (total_size_to_download > 1024 * 1024) + std::cout << "Total size to download: " + << total_size_to_download / (1024 * 1024) << " MB" + << std::endl; + else + std::cout << "Total to download: " << total_size_to_download + << " bytes" << std::endl; + } + for (size_t i = 0; i < to_download.size(); ++i) { + const auto &url = to_download[i]; + if (!quiet) { + if (dryRun) { + std::cout << "Would download "; + } else { + std::cout << "Downloading "; + } + std::cout << url << "... (" << i + 1 << " / " + << to_download.size() << ")" << std::endl; + } + if (!dryRun && + !proj_download_file(ctx, url.c_str(), false, nullptr, + nullptr)) { + std::cerr << "Cannot download " << url << std::endl; + std::exit(1); + } + } + + if (!queriedSourceId.empty() && !foundMatchSourceIdCriterion) { + std::cerr << "Warning: '" << queriedSourceId + << "' is a unknown value for --source-id." << std::endl; + std::cerr << "Known values are:" << std::endl; + for (const auto &v : source_ids) { + std::cerr << " " << v << std::endl; + } + std::exit(1); + } + + if (!queriedAreaOfUse.empty() && !foundMatchAreaOfUseCriterion) { + std::cerr << "Warning: '" << queriedAreaOfUse + << "' is a unknown value for --area-of-use." << std::endl; + std::cerr << "Known values are:" << std::endl; + for (const auto &v : areas_of_use) { + std::cerr << " " << v << std::endl; + } + std::exit(1); + } + + if (!queriedFilename.empty() && !foundMatchFileCriterion) { + std::cerr << "Warning: '" << queriedFilename + << "' is a unknown value for --file." << std::endl; + std::cerr << "Known values are:" << std::endl; + for (const auto &v : files) { + std::cerr << " " << v << std::endl; + } + std::exit(1); + } + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << std::endl; + std::exit(1); + } + + return 0; +} + +//! @endcond |
