Working With Angles in C++
September 21, 2024
Motivation #
I started stitching together a new map projections library from pieces of old code that were lying around. I wanted to make it as user friendly as possible so I started experimenting with techniques like fluent API. Considering that map projections need many parameters that are all numbers, an object constructor with a signature like this:
class Lambert {
public:
Lambert (double a, double f, double phi0, double lam0, double lat1, double lat2, double false_east, double false_north);
// ...
}
forces the user to write a line like:
//Parameters for Belgian BD72 projection
Lambert proj (6378388., 1/297., M_PI/2, 0.076227022370271014, 0.89302680060359663, 0.86975574391034338, 150000.01, 5400088.44);
This is far from user friendly as it requires user to permanently consult the constructor signature and convert the parameters from sexagesimal (Babylonian) degrees to radians.
Using fluent APi principles this can be turned into something friendlier:
class Lambert : public Projection {
public:
Lambert (const Projection::Params& pars);
// ...
}
There is a bit of wizardry hidden behind the Projection::Params
class (see below), but now user can write something like:
//Parameters for Belgian BD72 projection
Lambert proj (Projection::Params (Ellipsoid::Internatinal)
.ref_latitude (90*M_PI/180)
.ref_longitudde (4.36784667*M_PI/180)
.north_parallel (51.6666667*M_PI/180)
.south_parallel (49.8333333*M_PI/180)
.false_east (150000.01)
.false_north (5400088.44));
This is already much better, as one can easily see what each parameter is. There is still a nagging issue: angles have to be converted from the customary expressions of degrees, minutes, seconds.
Literal Operators for Angles #
Anyone who has worked a little bit with angles knows how annoying they can be: they can be expressed so many ways that only time comes close to them. To wit:
- radians - those are the units required by all math functions. In the end, you must get to those
- decimal degrees DD.ddddd - those are rather easy: just divide by 180 and multiply by π to get to radians
- degrees and decimal minutes DDMM.mmmmm - take the minutes part, divide by 60 and add the degrees to obtain decimal degrees.
- degrees, minutes and decimal seconds DDMMSS.ssss - seconds divided by 3600 added to minutes divided by 60 and added to degrees.
That’s without mentioning stuff like grads and milli-arcseconds, points or other exotic stuff like that.
So here is my idea: since C++11 we have literal operators that can be used. With a definition like:
constexpr double operator ""_dms (long double value)
{
int sign = (value >= 0) ? 1 : -1;
if (value < 0)
value = -value;
int deg = (int)(value / 10000.);
value -= (double)deg * 10000;
int min = (int)(value / 100.);
value -= (double)min * 100.;
return M_PI/180.*sign * (deg + min / 60. + value / 3600.);
}
We can now write:
angle = 42202.952_dms; //4°22'02.052"
Similar definitions can be made for _deg
and _dm
literal operators1.
Let’s add one more detail: since C++14 you can use single quotes inside a number literal to separate digits2. This brings us to the final form:
angle = 4'22'02.952_dms
Using these tricks, the Lambert projection for Belgian system BD72 can now be written:
//Parameters for Belgian BD72 projection
Lambert proj (Projection::Params (Ellipsoid::Internatinal)
.ref_latitude (90_deg)
.ref_longitudde (4'22'02.952_dms)
.north_parallel (51'10_dm)
.south_parallel (49'50_dm)
.false_east (150'000.01)
.false_north (5'400'088.44));
The Fluent API #
To get from that long, incomprehensible, list of parameters to a nice fluent API, I’ve used an object Projection::Params
that holds all the different parameters needed by a map projection. It has functions for setting individual parameters, returning itself as a return value. Here is a short fragment (simplified):
class Projection {
public:
class Params {
public:
Params (const Ellipsoid& ell);
Params& ref_latitude (double phi) {
par_[REF_LATITUDE] = phi;
return *this;
}
Params& ref_longitude (double lam) {
par_[REF_LONGITUDE] = lam;
return *this;
}
//...
}
}
There are important advantages to this approach:
- for each parameter it is clear what it represents
- parameters can be specified in any order
- any missing parameters can get reasonable defaults
Conclusion #
Using a fluent API together with user defined literal operators has produced a highly readable user interface. While the library is still in alpha stage, it looks rather promising.
In addition to literal operators for floating point values, we need also matching definitions for integer values. Their declarations look like these:
constexpr double operator ""_dms (unsigned long long val);
So far, I wasn’t able to combine floating point and integer functions in a single template. ↩︎See Integer literal ↩︎