using boost::karma to format latitude/longitude strings

倾然丶 夕夏残阳落幕 提交于 2019-12-22 07:08:10

问题


I need to format double values into coordinate strings that have a very specific format, "DDMMSS.SSX" where:

  • "DD" is the full degrees
  • "MM" is the full minutes
  • "SS.SS" is the seconds with fraction
  • "X" is either "N" or "S" depending on hemisphere

The fields need to be padded with zeroes. Spaces cannot be accepted. Examples for the formatting is as follows:

47.2535 ==> "471512.45N"
-0.123345 ==> "000724.04S"

I have managed to create the following program that does the job. However I have some questions:

  • is there a more elegant way for the locls rule? It's purpose is to store the absolute value into the local variable value. Is there a (hopefully more elegant) way to access the fabs() function?
  • In my opinion the assignments to _1 (_1 = _val etc.) are unnecessary since I have the value in the local variable value. However if I remove these assignments, all I get is "000000.00N".
  • the "workhorse" of this formatting is the int_ generator, which I use after calculating and casting the original value. Is there a better approach?
  • is there generally a better solution for this kind of problem?

I'd be glad for some feedback

#include <boost/spirit/include/karma.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/lambda/lambda.hpp>
#include <boost/bind.hpp>

namespace karma = boost::spirit::karma;

typedef std::back_insert_iterator<std::string> iterator_type;

struct genLongitude : karma::grammar<iterator_type, double()>
{
    genLongitude()
        :   genLongitude::base_type(start)
    {
        using karma::eps;
        using karma::int_;
        using karma::char_;
        using karma::_1;
        using karma::_val;
        using karma::right_align;
        using boost::phoenix::static_cast_;
        using boost::phoenix::ref;
        using boost::phoenix::if_;

        start = locls
                << degrees << minutes << seconds
                << ( eps(_val < 0.0) << char_('E') | char_('W')   );

        locls = eps[_1 = _val, if_(_val < 0.0) [ref(value) = - _val] .else_ [ref(value) = _val]];

        degrees = right_align(3,char_('0'))[int_[_1 = static_cast_<int>(ref(value))]]
                  << eps[ref(value) = (ref(value) - static_cast_<int>(ref(value))) * 60 ];

        minutes = right_align(2,char_('0'))[int_[_1 = static_cast_<int>(ref(value))]]
                  << eps[ref(value) = (ref(value) - static_cast_<int>(ref(value))) * 60 ];

        seconds = right_align(2,char_('0'))[int_[_1 = static_cast_<int>(ref(value))]]
                  << char_(".")
                  << eps[ref(value) = (ref(value) - static_cast_<int>(ref(value))) * 100 ]
                  << right_align(2,char_('0'))[int_[_1 = static_cast_<int>(ref(value))]];
    }

private:
    double value;

    karma::rule<iterator_type, double()>    start, locls, degrees, minutes, seconds;
};

int main()
{
    for(auto & value : std::vector<double>{ 47.25346, 13.984364, -0.1233453, -44.3 })
    {
        std::string generated;
        iterator_type outiter(generated);
        auto rv = karma::generate(outiter, genLatitude(), value);
        std::cout << "(" << rv << ") " << value << " ==> " << generated << std::endl;
    }
}

Update: Just for completeness, this is actually trivial to fix in any of the examples (and answers) The format of the Latitude is "DDMMSS.SSX" , the Longitude is "DDDMMSS.SSX". This is because range of the latitude is -90 to +90 while the longitude is -180 to +180.


回答1:


Giving it some more thought, let me answer

Q. is there generally a better solution for this kind of problem?

In this you may be better off with Boost Format. Reusing LatLongRep - the calculation work-horse from my other answer, you can create IO manipulators really easily:

namespace manip {
    struct LatLongRepIO : LatLongRep {
        LatLongRepIO(double val, char const* choices) : LatLongRep(val), _display(choices) { }
      private:
        char const* _display;

        friend std::ostream& operator<<(std::ostream& os, LatLongRepIO const& llr) {
            return os << boost::format("%03d%02d%05.2f%c")
                        % llr._deg % llr._min % llr._sec 
                        % (llr._display[llr._hemi]);
        }
    };

    LatLongRepIO as_latitude (double val) { return { val, "WE" }; }
    LatLongRepIO as_longitude(double val) { return { val, "NS" }; }
}

This forgoes the use of Boost Spirit, Phoenix and Fusion alltogether, and makes usage a breeze:

int main() {
    using namespace helpers::manip;

    for(double value : { 47.25346, 13.984364, -0.1233453, -44.3 })
        std::cout << as_latitude(value) << "\t" << as_longitude(value) << "\n";
}

DEMO

#include <boost/format.hpp>
#include <cmath>

namespace helpers {
    struct LatLongRep {
        bool _hemi; double _deg, _min, _sec;

        LatLongRep(double val) 
          : _hemi(0 < val),
            _min(60  * std::modf(std::abs(val), &_deg)),
            _sec(60  * std::modf(_min, &_min))
        { }
    };

    namespace manip {
        struct LatLongRepIO : LatLongRep {
            LatLongRepIO(double val, char const* choices) : LatLongRep(val), _display(choices) { }
          private:
            char const* _display;

            friend std::ostream& operator<<(std::ostream& os, LatLongRepIO const& llr) {
                return os << boost::format("%03d%02d%05.2f%c")
                            % llr._deg % llr._min % llr._sec 
                            % (llr._display[llr._hemi]);
            }
        };

        LatLongRepIO as_latitude (double val) { return { val, "WE" }; }
        LatLongRepIO as_longitude(double val) { return { val, "NS" }; }
    }
}

#include <iostream>

int main() {
    using namespace helpers::manip;

    for(double value : { 47.25346, 13.984364, -0.1233453, -44.3 })
        std::cout << as_latitude(value) << "\t" << as_longitude(value) << "\n";
}

Prints

0471512.46E  0471512.46S
0135903.71E  0135903.71S
0000724.04W  0000724.04N
0441760.00W  0441760.00N



回答2:


Separation of concerns.

Your grammar has become a mess because you're trying to stuff all logic in one place, that doesn't really afford it.

Meanwhile you've made the generator stateful, meaning that performance is down as well.

Instead, realize you have a mathematical transformation (real value) -> tuple(degrees, minutes, seconds, hemisphere). Let's create a tiny helper to model that:

struct LatLongRep {
    bool _hemi; double _deg, _min, _sec;

    LatLongRep(double val) 
      : _hemi(0 < val),
        _min(60  * std::modf(std::abs(val), &_deg)),
        _sec(60  * std::modf(_min, &_min))
        { }
};

Now, you can have rules like this:

karma::rule<iterator_type, LatLongRep()> latitude, longitude;

And they're trivially implemented:

    latitude =
            right_align(3, '0') [ uint_ ] 
         << right_align(2, '0') [ uint_ ] 
         << right_align(5, '0') [ seconds ]
         << east_west;

Demo

So the whole program becomes:

Live On Coliru

#include <boost/spirit/include/karma.hpp>
#include <boost/fusion/adapted/struct.hpp>
#include <cmath>

namespace karma = boost::spirit::karma;

typedef std::back_insert_iterator<std::string> iterator_type;

struct LatLongRep {
    bool _hemi; double _deg, _min, _sec;

    LatLongRep(double val) 
      : _hemi(0 < val),
        _min(60  * std::modf(std::abs(val), &_deg)),
        _sec(60  * std::modf(_min, &_min))
        { }
};

BOOST_FUSION_ADAPT_STRUCT(LatLongRep, _deg, _min, _sec, _hemi)

struct genLatLong : karma::grammar<iterator_type, double()> {
    genLatLong() : genLatLong::base_type(start)
    {
        using namespace karma;

        east_west.add  (true, 'E')(false, 'W');
        north_south.add(true, 'N')(false, 'S');

        start    = latitude;

        latitude =
                right_align(3, '0') [ uint_ ] 
             << right_align(2, '0') [ uint_ ] 
             << right_align(5, '0') [ seconds ]
             << east_west;

        longitude =
                right_align(3, '0') [ uint_ ] 
             << right_align(2, '0') [ uint_ ] 
             << right_align(5, '0') [ seconds ]
             << north_south;
    }

private:
    struct secfmt : karma::real_policies<double> {
        unsigned precision(double)      const { return 2;    }
        bool     trailing_zeros(double) const { return true; }
    };
    karma::real_generator<double, secfmt>    seconds;

    karma::symbols<bool, char> east_west, north_south;
    karma::rule<iterator_type, double()>     start;
    karma::rule<iterator_type, LatLongRep()> latitude, longitude;
};

int main()
{
    genLatLong const gen;

    for(auto & value : std::vector<double>{ 47.25346, 13.984364, -0.1233453, -44.3 })
    {
        std::string generated;
        iterator_type outiter(generated);
        auto rv = karma::generate(outiter, gen, value);
        std::cout << "(" << std::boolalpha << rv << ") " << value << " ==> " << generated << std::endl;
    }
}

Prints

(true) 47.2535 ==> 0471512.46E
(true) 13.9844 ==> 0135903.71E
(true) -0.123345 ==> 0000724.04W
(true) -44.3 ==> 0441760.00W

Additional notes/tricks:

  • using the derived real_policy named secfmt to format the seconds with 2 decimal places; see documentation

  • using fusion adaptation to get the fields of LatLongRep without excessive use of semantic actions and/or Phoenix binds (see tutorial example). See also Boost Spirit: "Semantic actions are evil"?

  • use of karma::symbols<> to format the hemisphere indicator:

    karma::symbols<bool, char> east_west, north_south;
    east_west.add  (true, 'E')(false, 'W');
    north_south.add(true, 'N')(false, 'S');
    
  • the generator construction is now out of the loop - which improves speed considerably

  • using both latitude and longitude as defined is left as an exercise for the reader



来源:https://stackoverflow.com/questions/32579406/using-boostkarma-to-format-latitude-longitude-strings

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!