using boost::karma to format latitude/longitude strings
Asked Answered
K

2

5

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.

Kutuzov answered 15/9, 2015 at 6:45 Comment(0)
C
3

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
Ciao answered 15/9, 2015 at 22:9 Comment(3)
Thanks again, @sehe. I've not had experience with boost::format so far, that's the main reason why I did not look into that direction. However, the final formatted string will not be used in a stream; it will be assigned to a xml-dom element (pugixml).Kutuzov
This still leaves me with the question: which of your both-brilliant answers should I accept?Kutuzov
Streaming and stringifying interfaces living together, without duplication and without being inefficient (c++14). The only thing missing now is using karma::auto_ , karma::stream to integrate with Boost Spirit :)Ciao
C
6

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

Ciao answered 15/9, 2015 at 13:2 Comment(1)
you're a genius! (+1 at least).Kutuzov
C
3

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
Ciao answered 15/9, 2015 at 22:9 Comment(3)
Thanks again, @sehe. I've not had experience with boost::format so far, that's the main reason why I did not look into that direction. However, the final formatted string will not be used in a stream; it will be assigned to a xml-dom element (pugixml).Kutuzov
This still leaves me with the question: which of your both-brilliant answers should I accept?Kutuzov
Streaming and stringifying interfaces living together, without duplication and without being inefficient (c++14). The only thing missing now is using karma::auto_ , karma::stream to integrate with Boost Spirit :)Ciao

© 2022 - 2024 — McMap. All rights reserved.