0
votes

I need to draw a geo-projected circle on the map. e.g. I want the center to be defined in terms of latitude and longitude and its radius to be defined in meters. I am using KDE Marble. In the API there is a function drawEllipse that takes a center as (lat, lon) and width and height of the ellipse.

GeoPainter::drawEllipse(const GeoDataCoordinates& center, qreal width, qreal height, bool isGeoProjected = false)

For a geo projected ellipse the width and height are considered to be in degrees. However I need them to be in meters. There is no simple conversion between degrees and meters because it depends on the position of the center in globe.

I need to convert the radius of the circle (in meters) to a pair of degree of displacement of a vector pointing to the center from the center of the earth.

I am also using boost geometry to do other geometrical calculation. Is there any function in boost-geometry that performs this conversion ?

UPDATE I

I first tried to have two GeoDataCoordinates one is the center and the other is on the perimeter. I expected that the difference between their latitudes and longitudes will work with the drawEllipse function.

painter->drawEllipse(_center, std::abs(_border.longitude(GeoDataCoordinates::Degree)-_center.longitude(GeoDataCoordinates::Degree)), std::abs(_border.latitude(GeoDataCoordinates::Degree)-_center.latitude(GeoDataCoordinates::Degree)), true);

However it produces an ellipse way smaller that I expected. The border point which was supposed to be on the circumference is beyond the ellipse. enter image description here

UPDATE II

Then I tried to use the central angle formula on wikipedia

double angular_distance = acos(
     (sin(_center.latitude(GeoDataCoordinates::Radian))*sin(_border.latitude(GeoDataCoordinates::Radian)))
    +(cos(_center.latitude(GeoDataCoordinates::Radian))*cos(_border.latitude(GeoDataCoordinates::Radian)))
    *(double)cos(std::abs(_border.longitude(GeoDataCoordinates::Radian)-_center.longitude(GeoDataCoordinates::Radian)))
);
painter->drawEllipse(_center, stmr::internal::rad2deg(angular_distance), stmr::internal::rad2deg(angular_distance), true);

enter image description here

The results are not much different from the previous one. However this time the ellipse is slightly larger than the previous one.

UPDATE III

The distance ratio between the arc and the sphere's great circle circumference is used to calculate the angular displacement in @ttemple's answer

painter->drawEllipse(_center, 2*(distance/earthRadiusKm)* 180.0/3.14159, 2*(distance/earthRadiusKm)* 180.0/3.14159, true);

produces a correct ellipse.

enter image description here

So I multiplied the angles with 2.0 with UPDATE II code and that also produced the similar (like UPDATE III) result.

UPDATE IV

But the problem with drawEllipse is that it actually draws a polygon with much fewer points on a zoomed out state. Sometimes it looks like a Square.

enter image description here

So it is a better option to draw more points on the ellipse circumference so that the fence looks like a circle in a zoomed out view too. The KDE Forum Link posted in the comments does the same.

GeoDataLineString ellipse;
qreal lon = 0.0;
qreal lat = 0.0;
int precision = 180;
for ( int i = 0; i < precision; ++i){
    qreal t = 1.0 - 2.0 * (qreal)(i) / (qreal)(precision);
    lat = center.latitude(GeoDataCoordinates::Degree)  + 0.5 * radius * sqrt(1.0 - t * t);
    lon = center.longitude(GeoDataCoordinates::Degree) + 0.5 * radius * t;
    ellipse << GeoDataCoordinates(lon, lat, 0.0, GeoDataCoordinates::Degree);
}
for ( int i = 0; i < precision; ++i){
    qreal t = 2.0 * (qreal)(i) / (qreal)(precision) - 1.0;
    lat = center.latitude(GeoDataCoordinates::Degree)  - 0.5 * radius * sqrt(1.0 - t * t);
    lon = center.longitude(GeoDataCoordinates::Degree) + 0.5 * radius * t;
    ellipse << GeoDataCoordinates(lon, lat, 0.0, GeoDataCoordinates::Degree);
}
painter->drawPolyline(ellipse);

However it draws a circle with much higher radius than the input. I think the snippet posted there is incomplete. There are unused conditional blocks that I ignored in my code. Also the code specifically mentions in comments that the radius of the fence has to be given in km. I did so. Also I didn't understand that math behind this. It doesn't use Earth Radius anywhere in the snippet. May be a minor correction to this code will produce a better ellipse. The math looks like some parametric curve which produces half of the ellipse. But there is no reference to the equation.

enter image description here

Also this works only in first quadrant where both lat and lon is positive

2
Did you try this example from KDE forum? forum.kde.org/viewtopic.php?f=217&t=111670Клаус Шварц
The NISTunits package may help, I’ve used it previously for unit conversions cran.r-project.org/web/packages/NISTunits/index.html.NColl
Should the circle be on the surface of the sphere, or projected above the sphere? (if that makes sense)ttemple
Projected on the surface. I am actually drawing a geofence.Neel Basu
Thanks for the KDE Forum Link. Not yet tried. Will check.Neel Basu

2 Answers

0
votes

The radius of the fence, as measured along the curve of the sphere, divided by the radius of the sphere is the angle in radians.

double circular_radius_of_fence = 1000;    // circular radius of fence
double sphere_radius = 6378000;   // radius of sphere, meters

// included angle in radians
double ang_radians = circular_radius_of_fence / sphere_radius;

// convert to degrees
double ang_degrees = ang_radians * 180.0/PI;

// width and height of circle is twice the angle
double width = ang_degrees * 2;
double height = ang_degrees * 2;
0
votes

The answer in the KDE Forum actually the implementation in GeoPainter::drawEllipse.

const int precision = qMin<qreal>( width / degreeResolution / 8 + 1, 81 )

This line in that implementation reduces the precision such that it looks like a square, when zoomed out. So I copied the drawEllipse code with single line modification for higher precision.

void FenceLayer::drawFence(Marble::GeoPainter* painter, Marble::GeoDataCoordinates center, double radius){
    double height = radius;
    double width  = radius;
    // Initialize variables
    const qreal centerLon = center.longitude( GeoDataCoordinates::Degree );
    const qreal centerLat = center.latitude( GeoDataCoordinates::Degree );
    const qreal altitude = center.altitude();

    // Ensure a valid latitude range: 
    if ( centerLat + 0.5 * height > 90.0 || centerLat - 0.5 * height < -90.0 ) {
        return;
    }

    // Don't show the ellipse if it's too small:
    GeoDataLatLonBox ellipseBox( centerLat + 0.5 * height, centerLat - 0.5 * height,
                            centerLon + 0.5 * width,  centerLon - 0.5 * width, 
                            GeoDataCoordinates::Degree );
    if ( !_map->viewport()->viewLatLonAltBox().intersects( ellipseBox ) ||
    !_map->viewport()->resolves( ellipseBox ) ) return;

    GeoDataLinearRing ellipse;

    // Optimizing the precision by determining the size which the 
    // ellipse covers on the screen:
    const qreal degreeResolution = _map->viewport()->angularResolution() * RAD2DEG;
    // To create a circle shape even for very small precision we require uneven numbers:
    const int scaled_resolution = qMin<qreal>( width / degreeResolution / 8 + 1, 81 );
    const int precision = qMin<qreal>( width / degreeResolution / 8 + 1, 81 ) < 81 ? 81 : scaled_resolution ;

    // Calculate the shape of the upper half of the ellipse:
    for ( int i = 0; i <= precision; ++i ) {
        const qreal t = 1.0 - 2.0 * (qreal)(i) / (qreal)(precision);
        const qreal lat = centerLat + 0.5 * height * sqrt( 1.0 - t * t );
        const qreal lon = centerLon + 0.5 * width * t;
        ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree );
    }
    // Calculate the shape of the lower half of the ellipse:
    for ( int i = 0; i <= precision; ++i ) {
        const qreal t = 2.0 * (qreal)(i) / (qreal)(precision) -  1.0;
        const qreal lat = centerLat - 0.5 * height * sqrt( 1.0 - t * t );
        const qreal lon = centerLon + 0.5 * width * t;
        ellipse << GeoDataCoordinates( lon, lat, altitude, GeoDataCoordinates::Degree );
    }

    painter->drawPolygon(ellipse);
}

While calling drawFence I convert distance to degree of displacement as suggested in @ttemple's answer.

drawFence(painter, _center, 2*(distance/earthRadiusKm)* 180.0/3.14159);

Where distance is the radius of the fence.