diff --git a/CMakeLists.txt b/CMakeLists.txt index 62aebc9979..77def8a7b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -415,6 +415,8 @@ if(BUILD_EXAMPLES) add_subdirectory(examples) endif() +add_subdirectory(scripts) + set(docfiles COPYING NEWS.md AUTHORS.md) install(FILES ${docfiles} DESTINATION ${CMAKE_INSTALL_DOCDIR}) @@ -459,7 +461,6 @@ set(CPACK_SOURCE_IGNORE_FILES /m4/libtool* /media/ /schemas/ - /scripts/ /test/fuzzers/ /test/gigs/.*gie\\.failing /test/postinstall/ diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt new file mode 100644 index 0000000000..8de02a3dda --- /dev/null +++ b/scripts/CMakeLists.txt @@ -0,0 +1,28 @@ +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(PC_BASH_COMPLETION QUIET bash-completion) + if (PC_BASH_COMPLETION_FOUND) + pkg_get_variable(BASH_COMPLETIONS_FULL_DIR bash-completion completionsdir) + pkg_get_variable(BASH_COMPLETIONS_PREFIX bash-completion prefix) + if (BASH_COMPLETIONS_FULL_DIR + AND BASH_COMPLETIONS_PREFIX + AND BASH_COMPLETIONS_FULL_DIR MATCHES "^${BASH_COMPLETIONS_PREFIX}/") + string(REGEX REPLACE "^${BASH_COMPLETIONS_PREFIX}/" "" BASH_COMPLETIONS_DIR_DEFAULT ${BASH_COMPLETIONS_FULL_DIR}) + endif () + endif () +endif () + +if (NOT DEFINED BASH_COMPLETIONS_DIR_DEFAULT) + include(GNUInstallDirs) + set(BASH_COMPLETIONS_DIR_DEFAULT ${CMAKE_INSTALL_DATADIR}/bash-completion/completions) +endif () + +set(BASH_COMPLETIONS_DIR + "${BASH_COMPLETIONS_DIR_DEFAULT}" + CACHE PATH "Installation sub-directory for bash completion scripts") + +if (NOT BASH_COMPLETIONS_DIR STREQUAL "") + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/install_bash_completions.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/install_bash_completions.cmake @ONLY) + install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/install_bash_completions.cmake) +endif () diff --git a/scripts/install_bash_completions.cmake.in b/scripts/install_bash_completions.cmake.in new file mode 100644 index 0000000000..a7b2541d77 --- /dev/null +++ b/scripts/install_bash_completions.cmake.in @@ -0,0 +1,13 @@ +set(PROGRAMS + projinfo +) + +set(INSTALL_DIR "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@BASH_COMPLETIONS_DIR@") + +file(MAKE_DIRECTORY "${INSTALL_DIR}") + +foreach (program IN LISTS PROGRAMS) + message(STATUS "Installing ${INSTALL_DIR}/${program}") + configure_file("@CMAKE_CURRENT_SOURCE_DIR@/${program}-bash-completion.sh" "${INSTALL_DIR}/${program}" COPYONLY) + file(APPEND "@PROJECT_BINARY_DIR@/install_manifest_extra.txt" "${INSTALL_DIR}/${program}\n") +endforeach () diff --git a/scripts/projinfo-bash-completion.sh b/scripts/projinfo-bash-completion.sh new file mode 100755 index 0000000000..4027bf9125 --- /dev/null +++ b/scripts/projinfo-bash-completion.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +function_exists() { + declare -f -F "$1" > /dev/null + return $? +} + +# Checks that bash-completion is recent enough +function_exists _get_comp_words_by_ref || return 0 + +_projinfo() +{ + local cur prev + COMPREPLY=() + _get_comp_words_by_ref cur prev + choices=$(projinfo completion ${COMP_LINE}) + if [[ "$cur" == "=" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$cur" == ":" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "${cur: -1}" == "/" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "${cur: -2}" == "/ " ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "${cur: -1}" == "+" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "${cur: -2}" == "+ " ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$cur" == "!" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" -P "! " --) + else + mapfile -t COMPREPLY < <(compgen -W "$choices" -- "$cur") + fi + for element in "${COMPREPLY[@]}"; do + if [[ $element == */ ]]; then + # Do not add a space if one of the suggestion ends with slash + compopt -o nospace + break + elif [[ $element == *= ]]; then + # Do not add a space if one of the suggestion ends with equal + compopt -o nospace + break + elif [[ $element == *: ]]; then + # Do not add a space if one of the suggestion ends with colon + compopt -o nospace + break + fi + done +} +complete -o default -F _projinfo projinfo + diff --git a/src/apps/projinfo.cpp b/src/apps/projinfo.cpp index f32f9964a1..a7419fbfb5 100644 --- a/src/apps/projinfo.cpp +++ b/src/apps/projinfo.cpp @@ -1053,6 +1053,340 @@ static void outputOperations( // --------------------------------------------------------------------------- +static void suggestCompletion(const std::vector &args) { +#ifdef DEBUG_COMPLETION + for (const auto &arg : args) + fprintf(stderr, "'%s' ", arg.c_str()); + fprintf(stderr, "\n"); +#endif + + for (const std::string &arg : args) { + // Shouldn't happen, unless someone really tries to actively crash us + if (arg.empty()) + return; + } + + bool first = true; + if (args.empty()) { + try { + auto dbContext = DatabaseContext::create(); + for (const auto &authName : dbContext->getAuthorities()) { + if (!first) + printf(" "); + first = false; + printf("%s:", authName.c_str()); + } + printf("\n"); + } catch (const std::exception &) { + } + return; + } else if (args.size() == 1 && args[0].front() != '-' && + args[0].find(':') == std::string::npos) { + try { + auto dbContext = DatabaseContext::create(); + for (const auto &authName : dbContext->getAuthorities()) { + if (starts_with(authName, args[0])) { + if (!first) + printf(" "); + first = false; + printf("%s:", authName.c_str()); + } + } + } catch (const std::exception &) { + } + } + + const auto isOption = [&args](const char *opt) { + return args.back() == opt || + (args.size() >= 2 && args[args.size() - 2] == opt); + }; + + if (isOption("-k")) { + printf("crs operation datum ensemble ellipsoid\n"); + return; + } + + if (isOption("-o")) { + if (starts_with(args.back(), "WKT1:")) + printf("GDAL ESRI\n"); + else if (starts_with(args.back(), "WKT2:")) + printf("2019 2015\n"); + else + printf("all PROJ WKT2:2019 WKT2:2015 WKT1:GDAL WKT1:ESRI PROJJSON " + "SQL\n"); + return; + } + + if (isOption("--spatial-test")) { + printf("contains intersects\n"); + return; + } + + if (isOption("--crs-extent-use")) { + printf("none both intersection smallest\n"); + return; + } + + if (isOption("--grid-check")) { + printf("none discard_missing sort known_available\n"); + return; + } + + if (isOption("--pivot-crs")) { + if (args.back().back() == ':') + return; + printf("always if_no_direct_transformation never"); + try { + auto dbContext = DatabaseContext::create(); + for (const auto &authName : dbContext->getAuthorities()) { + printf(" %s:", authName.c_str()); + } + printf("\n"); + } catch (const std::exception &) { + } + return; + } + + if (args.back()[0] == '-') { + const char *const knownOptions[] = { + "-o", + "-k", + "--summary", + "-q", + "--area", + "--bbox", + "--spatial-test", + "--crs-extent-use", + "--grid-check", + "--pivot-crs", + "--show-superseded", + "--hide-ballpark", + "--accuracy", + "--allow-ellipsoidal-height-as-vertical-crs", + "--boundcrs-to-wgs84", + "--authority", + "--main-db-path", + "--aux-db-path", + "--identify", + "--3d", + "--output-id", + "--c-ify", + "--single-line", + "--searchpaths", + "--remote-data", + "--list-crs", + "--dump-db-structure", + "-s", + "--s_epoch", + "-t", + "--t_epoch", + }; + + for (const char *opt : knownOptions) { + if (args.back() == opt) + return; + } + for (const char *opt : knownOptions) { + if (!first) + printf(" "); + first = false; + printf("%s", opt); + } + printf("\n"); + return; + } + + std::string lastArg = args.back(); + for (size_t i = args.size(); i >= 1;) { + --i; + if (args[i].size() >= 2 && args[i].back() == '"') { + break; + } + if (args[i].size() >= 2 && args[i][0] == '"') { + lastArg = args[i].substr(1); + ++i; + for (; i < args.size(); ++i) { + lastArg += " "; + lastArg += args[i]; + } + break; + } + } +#ifdef DEBUG_COMPLETION + fprintf(stderr, "lastArg='%s'\n", lastArg.c_str()); +#endif + + try { + auto dbContext = DatabaseContext::create(); + const auto columnPos = args.back().find(':'); + if (columnPos != std::string::npos) { + const auto authName = args.back().substr(0, columnPos); + const auto codeStart = columnPos + 1 < args.back().size() + ? args.back().substr(columnPos + 1) + : std::string(); + auto factory = AuthorityFactory::create(dbContext, authName); + const auto list = factory->getCRSInfoList(); + + std::vector res; + std::string code; + for (const auto &info : list) { + if (!info.deprecated && + (codeStart.empty() || starts_with(info.code, codeStart))) { + if (res.empty()) + code = info.code; + res.push_back(std::string(info.code).append(" -- ").append( + info.name)); + } + } + if (res.size() == 1) { + // If there is a single match, remove the name from the + // suggestion. + res.clear(); + res.push_back(code); + } + for (const auto &val : res) { + if (!first) + printf(" "); + first = false; + printf("%s", replaceAll(val, " ", "\\ ").c_str()); + } + printf("\n"); + return; + } + + for (const char *authName : {"EPSG", ""}) { + auto factory = + AuthorityFactory::create(dbContext, std::string(authName)); + const auto list = factory->getCRSInfoList(); + for (const auto &info : list) { + if (!info.deprecated && starts_with(info.name, lastArg)) { + if (!first) + printf(" "); + first = false; + std::string val = info.name; + if (args.back() == "+" || args.back() == "/") { + const auto pos = val.find(args.back()[0]); + if (pos != std::string::npos && pos + 1 < val.size() && + val[pos + 1] == ' ') + val = val.substr(pos + 2); + } + printf("%s", replaceAll(val, " ", "\\ ").c_str()); + } + } + if (!first) { + printf("\n"); + break; + } + } + + // If the input was ``projinfo "NAD83(HARN) / California Albers +``, + // then check if "NAD83(HARN) / California Albers" is a known horizontal + // CRS, and if so, suggest relevant potential vertical CRS. + const auto posSpacePlus = lastArg.find(" +"); + if (first && posSpacePlus != std::string::npos) { + const std::string candidateHorizCRSName = + lastArg.substr(0, posSpacePlus); + auto factory = AuthorityFactory::create(dbContext, std::string()); +#ifdef DEBUG_COMPLETION + fprintf(stderr, "candidateHorizCRSName='%s'\n", + candidateHorizCRSName.c_str()); +#endif + const auto candidateHorizCRS = factory->createObjectsFromName( + candidateHorizCRSName, + {AuthorityFactory::ObjectType::GEOGRAPHIC_2D_CRS, + AuthorityFactory::ObjectType::PROJECTED_CRS}, + /* approximateMatch = */ true, + /* limitResultCount = */ 2); + if (!candidateHorizCRS.empty()) { + const auto &domains = dynamic_cast( + candidateHorizCRS.front().get()) + ->domains(); + if (domains.size() == 1) { + const auto &domain = domains[0]->domainOfValidity(); + if (domain && domain->geographicElements().size() == 1) { + const auto bbox = + dynamic_cast( + domain->geographicElements()[0].get()); + if (bbox) { + std::string vertCRSAuthName; + const auto &codeSpace = candidateHorizCRS.front() + ->identifiers() + .front() + ->codeSpace(); + if (codeSpace.has_value()) + vertCRSAuthName = *codeSpace; + auto factoryVertCRS = AuthorityFactory::create( + dbContext, vertCRSAuthName); + const auto list = factoryVertCRS->getCRSInfoList(); + std::string horizAreaOfUse; + if (domain->description().has_value()) { + horizAreaOfUse = *(domain->description()); + const auto posDash = horizAreaOfUse.find(" -"); + if (posDash != std::string::npos) + horizAreaOfUse.resize(posDash); + } + for (size_t attempt = 0; first && attempt < 2; + ++attempt) { + for (const auto &info : list) { + if (!info.deprecated && info.bbox_valid && + info.type == + AuthorityFactory::ObjectType:: + VERTICAL_CRS && + !starts_with(info.name, + "EPSG example")) { + std::string vertAreaOfUse = + info.areaName; + const auto posDash = + vertAreaOfUse.find(" -"); + if (posDash != std::string::npos) + vertAreaOfUse.resize(posDash); + bool ok = false; + if (attempt == 0 && + !horizAreaOfUse.empty()) { + ok = + horizAreaOfUse == vertAreaOfUse; + } else if (attempt == 1 && + vertAreaOfUse == "World.") { + // auto vertCrsBbox = + // GeographicBoundingBox::create(info.west_lon_degree, + // info.south_lat_degree, + // info.east_lon_degree, + // info.north_lat_degree); if( + // bbox->intersects(vertCrsBbox)) + { ok = true; } + } + if (ok) { + if (!first) + printf(" "); + first = false; +#ifdef DEBUG_COMPLETION + fprintf(stderr, "'%s'\n", + replaceAll(info.name, " ", + "\\ ") + .c_str()); +#endif + printf("%s", replaceAll(info.name, + " ", "\\ ") + .c_str()); + } + } + } + if (!first) { + printf("\n"); + } + } + } + } + } + } + } + + } catch (const std::exception &) { + } +} + +// --------------------------------------------------------------------------- + int main(int argc, char **argv) { pj_stderr_proj_lib_deprecation_warning(); @@ -1062,6 +1396,11 @@ int main(int argc, char **argv) { usage(); } + if (argc >= 3 && strcmp(argv[1], "completion") == 0) { + suggestCompletion(std::vector(argv + 3, argv + argc)); + return 0; + } + std::vector positional_args; std::string sourceCRSStr; std::string sourceEpoch; diff --git a/test/cli/test_projinfo.yaml b/test/cli/test_projinfo.yaml index 74e3cbbffb..26dad584de 100644 --- a/test/cli/test_projinfo.yaml +++ b/test/cli/test_projinfo.yaml @@ -1890,3 +1890,41 @@ tests: out: | Candidate operations found: 1 unknown id, Conversion from WGS 84 (G1150) (geog2D) to WGS 84 (G1150) (geocentric) + WGS 84 (G1150) to WGS 84 (G1762) (1) + WGS 84 (G1762) to WGS 84 (G2139) (1) + WGS 84 (G2139) to WGS 84 (G2296) (1) + Conversion from WGS 84 (G2296) (geocentric) to WGS 84 (G2296) (geog2D), 0.04 m, World +- args: completion projinfo - + out: | + -o -k --summary -q --area --bbox --spatial-test --crs-extent-use --grid-check --pivot-crs --show-superseded --hide-ballpark --accuracy --allow-ellipsoidal-height-as-vertical-crs --boundcrs-to-wgs84 --authority --main-db-path --aux-db-path --identify --3d --output-id --c-ify --single-line --searchpaths --remote-data --list-crs --dump-db-structure -s --s_epoch -t --t_epoch +- args: completion projinfo -o + out: all PROJ WKT2:2019 WKT2:2015 WKT1:GDAL WKT1:ESRI PROJJSON SQL +- args: completion projinfo -o "WKT2:" + out: 2019 2015 +- args: completion projinfo -o "WKT1:" + out: GDAL ESRI +- args: completion projinfo --pivot-crs + out: | + always if_no_direct_transformation never EPSG: ESRI: IAU_2015: IGNF: NKG: NRCAN: OGC: PROJ: +- args: completion projinfo + out: | + EPSG: ESRI: IAU_2015: IGNF: NKG: NRCAN: OGC: PROJ: +- args: completion projinfo NKG + out: "NKG:" +- args: completion projinfo "OGC:" + out: | + CRS27\ --\ NAD27\ (CRS27) CRS83\ --\ NAD83\ (CRS83) CRS84\ --\ WGS\ 84\ (CRS84) CRS84h\ --\ WGS\ 84\ longitude-latitude-height +- args: completion projinfo EPSG:432 + out: | + 4322\ --\ WGS\ 72 4324\ --\ WGS\ 72BE 4326\ --\ WGS\ 84 +- args: completion projinfo EPSG:4326 + out: | + 4326 +- args: completion projinfo EGM + out: | + EGM2008\ height EGM96\ height EGM84\ height +- args: completion projinfo "\"RGF93" v1 "/" + out: | + Lambert-93 CC42 CC43 CC44 CC45 CC46 CC47 CC48 CC49 CC50 Lambert-93\ +\ NGF-IGN69\ height Lambert-93\ +\ NGF-IGN78\ height +- args: completion projinfo "\"NAD83(HARN) / California Albers" "+" + out: | + NGVD29\ height\ (ftUS) NAVD88\ depth\ (ftUS) NGVD29\ depth\ (ftUS) NAVD88\ height\ (ftUS) NGVD29\ height\ (m) MSL\ height\ (ftUS) MSL\ depth\ (ftUS) NAVD88\ height\ (ft) +- args: completion projinfo "\"Mauritania 1999" "/" UTM zone 29N "+" + out: | + EGM2008\ height MSL\ height MSL\ depth EGM96\ height EGM84\ height Instantaneous\ Water\ Level\ height Instantaneous\ Water\ Level\ depth LAT\ depth LLWLT\ depth ISLW\ depth MLLWS\ depth MLWS\ depth MLLW\ depth MLW\ depth MHW\ height MHHW\ height MHWS\ height HHWLT\ height HAT\ height Low\ Water\ depth High\ Water\ height MSL\ height\ (ft) MSL\ depth\ (ft)