/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2023-05-15
 * Description : geolocation engine based on Marble.
 *               (c) 2007-2022 Marble Team
 *               https://invent.kde.org/education/marble/-/raw/master/data/credits_authors.html
 *
 * SPDX-FileCopyrightText: 2023-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * ============================================================ */

#include "GeoDataLatLonBox.h"

// Qt includes

#include <QDataStream>

// Local includes

#include "GeoDataLineString.h"
#include "GeoDataTypes.h"
#include "digikam_debug.h"

namespace Marble
{

const GeoDataLatLonBox GeoDataLatLonBox::empty = GeoDataLatLonBox();

class Q_DECL_HIDDEN GeoDataLatLonBoxPrivate
{
public:

    GeoDataLatLonBoxPrivate()
        : m_north(0.0),
          m_south(0.0),
          m_east(0.0),
          m_west(0.0),
          m_rotation(0.0)
    {
    }

    qreal m_north;
    qreal m_south;
    qreal m_east;
    qreal m_west;
    qreal m_rotation; // NOT implemented yet!
};

bool operator==(GeoDataLatLonBox const& lhs, GeoDataLatLonBox const& rhs)
{
    return lhs.d->m_west == rhs.d->m_west &&
           lhs.d->m_east == rhs.d->m_east &&
           lhs.d->m_north == rhs.d->m_north &&
           lhs.d->m_south == rhs.d->m_south &&
           lhs.d->m_rotation == rhs.d->m_rotation;
}

bool operator!=(GeoDataLatLonBox const& lhs, GeoDataLatLonBox const& rhs)
{
    return !(lhs == rhs);
}

GeoDataLatLonBox::GeoDataLatLonBox()
    : GeoDataObject(),
      d(new GeoDataLatLonBoxPrivate)
{
}

GeoDataLatLonBox::GeoDataLatLonBox(qreal north, qreal south, qreal east, qreal west, GeoDataCoordinates::Unit unit)
    : GeoDataObject(),
      d(new GeoDataLatLonBoxPrivate)
{
    setBoundaries(north, south, east, west, unit);
}

GeoDataLatLonBox::GeoDataLatLonBox(const GeoDataLatLonBox& other)
    : GeoDataObject(other),
      d(new GeoDataLatLonBoxPrivate(*other.d))
{
}

GeoDataLatLonBox::~GeoDataLatLonBox()
{
    delete d;
}

const char* GeoDataLatLonBox::nodeType() const
{
    return GeoDataTypes::GeoDataLatLonBoxType;
}

qreal GeoDataLatLonBox::north(GeoDataCoordinates::Unit unit) const
{
    if (unit == GeoDataCoordinates::Degree)
    {
        return d->m_north * RAD2DEG;
    }

    return d->m_north;
}

void GeoDataLatLonBox::setNorth(const qreal north, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_north = GeoDataCoordinates::normalizeLat(north);
            break;

        case GeoDataCoordinates::Degree:
            d->m_north = GeoDataCoordinates::normalizeLat(north * DEG2RAD);
            break;
    }
}

qreal GeoDataLatLonBox::south(GeoDataCoordinates::Unit unit) const
{
    if (unit == GeoDataCoordinates::Degree)
    {
        return d->m_south * RAD2DEG;
    }

    return d->m_south;
}

void GeoDataLatLonBox::setSouth(const qreal south, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_south = GeoDataCoordinates::normalizeLat(south);
            break;

        case GeoDataCoordinates::Degree:
            d->m_south = GeoDataCoordinates::normalizeLat(south * DEG2RAD);
            break;
    }
}

qreal GeoDataLatLonBox::east(GeoDataCoordinates::Unit unit) const
{
    if (unit == GeoDataCoordinates::Degree)
    {
        return d->m_east * RAD2DEG;
    }

    return d->m_east;
}

void GeoDataLatLonBox::setEast(const qreal east, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_east = GeoDataCoordinates::normalizeLon(east);
            break;

        case GeoDataCoordinates::Degree:
            d->m_east = GeoDataCoordinates::normalizeLon(east * DEG2RAD);
            break;
    }
}

qreal GeoDataLatLonBox::west(GeoDataCoordinates::Unit unit) const
{
    if (unit == GeoDataCoordinates::Degree)
    {
        return d->m_west * RAD2DEG;
    }

    return d->m_west;
}

void GeoDataLatLonBox::setWest(const qreal west, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_west = GeoDataCoordinates::normalizeLon(west);
            break;

        case GeoDataCoordinates::Degree:
            d->m_west = GeoDataCoordinates::normalizeLon(west * DEG2RAD);
            break;
    }
}

void GeoDataLatLonBox::setRotation(const qreal rotation, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_rotation = rotation;
            break;

        case GeoDataCoordinates::Degree:
            d->m_rotation = rotation * DEG2RAD;
            break;
    }
}

qreal GeoDataLatLonBox::rotation(GeoDataCoordinates::Unit unit) const
{
    if (unit == GeoDataCoordinates::Degree)
    {
        return d->m_rotation * RAD2DEG;
    }

    return d->m_rotation;
}

void GeoDataLatLonBox::boundaries(qreal& north, qreal& south, qreal& east, qreal& west, GeoDataCoordinates::Unit unit) const
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            north = d->m_north;
            south = d->m_south;
            east  = d->m_east;
            west  = d->m_west;
            break;

        case GeoDataCoordinates::Degree:
            north = d->m_north * RAD2DEG;
            south = d->m_south * RAD2DEG;
            east  = d->m_east  * RAD2DEG;
            west  = d->m_west  * RAD2DEG;
            break;
    }
}

void GeoDataLatLonBox::setBoundaries(qreal north, qreal south, qreal east, qreal west, GeoDataCoordinates::Unit unit)
{
    switch (unit)
    {
        default:
        case GeoDataCoordinates::Radian:
            d->m_north = GeoDataCoordinates::normalizeLat(north);
            d->m_south = GeoDataCoordinates::normalizeLat(south);
            d->m_east =  GeoDataCoordinates::normalizeLon(east);
            d->m_west =  GeoDataCoordinates::normalizeLon(west);
            break;

        case GeoDataCoordinates::Degree:
            d->m_north = GeoDataCoordinates::normalizeLat(north * DEG2RAD);
            d->m_south = GeoDataCoordinates::normalizeLat(south * DEG2RAD);
            d->m_east =  GeoDataCoordinates::normalizeLon(east * DEG2RAD);
            d->m_west =  GeoDataCoordinates::normalizeLon(west * DEG2RAD);
            break;
    }
}

void GeoDataLatLonBox::scale(qreal verticalFactor, qreal horizontalFactor) const
{
    GeoDataCoordinates const middle = center();
    qreal const deltaY = 0.5 * height() * verticalFactor;
    qreal const deltaX = 0.5 * width() * horizontalFactor;
    d->m_north = qMin((middle.latitude() + deltaY), static_cast<qreal>(M_PI / 2));
    d->m_south = qMax((middle.latitude() - deltaY), static_cast<qreal>(-M_PI / 2));

    if (deltaX > 180 * DEG2RAD)
    {
        d->m_east = M_PI;
        d->m_west = -M_PI;
    }

    else
    {
        d->m_east = GeoDataCoordinates::normalizeLon(middle.longitude() + deltaX);
        d->m_west = GeoDataCoordinates::normalizeLon(middle.longitude() - deltaX);
    }
}

GeoDataLatLonBox GeoDataLatLonBox::scaled(qreal verticalFactor, qreal horizontalFactor) const
{
    GeoDataLatLonBox result = *this;
    result.scale(verticalFactor, horizontalFactor);
    return result;
}

qreal GeoDataLatLonBox::width(GeoDataCoordinates::Unit unit) const
{
    return GeoDataLatLonBox::width(d->m_east, d->m_west, unit);
}

qreal GeoDataLatLonBox::width(qreal east, qreal west, GeoDataCoordinates::Unit unit)
{
    qreal width = fabs((qreal)(GeoDataLatLonBox::crossesDateLine(east, west)
                               ? 2 * M_PI - west + east
                               : east - west));

    // This also covers the case where this bounding box covers the whole
    // longitude range ( -180 <= lon <= + 180 ).
    if (width > 2 * M_PI)
    {
        width = 2 * M_PI;
    }

    if (unit == GeoDataCoordinates::Degree)
    {
        return width * RAD2DEG;
    }

    return width;
}

qreal GeoDataLatLonBox::height(GeoDataCoordinates::Unit unit) const
{
    return GeoDataLatLonBox::height(d->m_north, d->m_south, unit);
}

qreal GeoDataLatLonBox::height(qreal north, qreal south, GeoDataCoordinates::Unit unit)
{
    qreal height = fabs((qreal)(south - north));

    if (unit == GeoDataCoordinates::Degree)
    {
        return height * RAD2DEG;
    }

    return height;
}

bool GeoDataLatLonBox::crossesDateLine() const
{
    return GeoDataLatLonBox::crossesDateLine(d->m_east, d->m_west);
}

bool GeoDataLatLonBox::crossesDateLine(qreal east, qreal west)
{
    return east < west || (east == M_PI && west == -M_PI);
}

GeoDataCoordinates GeoDataLatLonBox::center() const
{
    if (isEmpty())
    {
        return GeoDataCoordinates();
    }

    if (crossesDateLine())
        return GeoDataCoordinates(GeoDataCoordinates::normalizeLon(east() + 2 * M_PI - (east() + 2 * M_PI - west()) / 2),
                                  north() - (north() - south()) / 2);

    else
        return GeoDataCoordinates(east() - (east() - west()) / 2,
                                  north() - (north() - south()) / 2);
}

bool GeoDataLatLonBox::containsPole(Pole pole) const
{
    switch (pole)
    {
        case NorthPole:
            return (2 * north() == +M_PI);

        case SouthPole:
            return (2 * south() == -M_PI);

        default:
        case AnyPole:
            return (2 * north() == +M_PI
                    || 2 * south() == -M_PI);
    }

    qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << "Invalid pole";
    return false;
}

bool GeoDataLatLonBox::contains(qreal lon, qreal lat) const
{
    if (lat < d->m_south || lat > d->m_north)
    {
        return false;
    }

    // We need to take care of the normal case ...
    if (((lon < d->m_west || lon > d->m_east) && (d->m_west < d->m_east)) ||
        // ... and the case where the bounding box crosses the date line:
        ((lon < d->m_west && lon > d->m_east) && (d->m_west > d->m_east)))
    {
        return false;
    }

    return true;
}

bool GeoDataLatLonBox::contains(const GeoDataCoordinates& point) const
{
    qreal lon, lat;

    point.geoCoordinates(lon, lat);

    return contains(lon, lat);
}

bool GeoDataLatLonBox::contains(const GeoDataLatLonBox& other) const
{
    // check the contain criterion for the latitude first as this is trivial:

    if (d->m_north >= other.north() && d->m_south <= other.south())
    {

        if (!crossesDateLine())
        {
            if (!other.crossesDateLine())
            {
                // "Normal" case: both bounding boxes don't cross the date line
                if (d->m_west <= other.west() && d->m_east >= other.east())
                {
                    return true;
                }
            }

            else
            {
                // The other bounding box crosses the date line, "this" one does not:
                // So the date line splits the other bounding box in two parts.
                // Hence "this" bounding box could be fully contained by one of them.
                // So for both cases we are able to ignore the "overhanging" portion
                // and thereby basically reduce the problem to the "normal" case:

                if ((other.west() <= d->m_west && d->m_east <= +M_PI)
                    || (other.east() >= d->m_east && d->m_west >= -M_PI))
                {
                    return true;
                }
            }
        }

        else
        {
            if (other.crossesDateLine())
            {
                // Other "Simple" case: both bounding boxes cross the date line
                if (d->m_west <= other.west() && d->m_east >= other.east())
                {
                    return true;
                }
            }

            else
            {
                // "This" bounding box crosses the date line, the other one does not.
                // So the date line splits "this" bounding box in two parts.
                // Hence the other bounding box could be fully contained by one of them.
                // So for both cases we are able to ignore the "overhanging" portion
                // and thereby basically reduce the problem to the "normal" case:

                if ((d->m_west <= other.west() && other.east() <= +M_PI)
                    || (d->m_east >= other.east() && other.west() >= -M_PI))
                {
                    return true;
                }

                // if this bounding box covers the whole longitude range  ( -180 <= lon <= + 180 )
                // then of course the "inner" bounding box is "inside"
                if (d->m_west == -M_PI && d->m_east == +M_PI)
                {
                    return true;
                }
            }

        }
    }

    return false;
}

bool GeoDataLatLonBox::intersects(const GeoDataLatLonBox& other) const
{
    if (isEmpty() || other.isEmpty())
    {
        return false;
    }

    // check the intersection criterion for the latitude first:

    // Case 1: northern boundary of other box intersects:
    if ((d->m_north >= other.d->m_north && d->m_south <= other.d->m_north)
        // Case 2: northern boundary of this box intersects:
        || (other.d->m_north >= d->m_north && other.d->m_south <= d->m_north)
        // Case 3: southern boundary of other box intersects:
        || (d->m_north >= other.d->m_south && d->m_south <= other.d->m_south)
        // Case 4: southern boundary of this box intersects:
        || (other.d->m_north >= d->m_south && other.d->m_south <= d->m_south))
    {

        if (!crossesDateLine())
        {
            if (!other.crossesDateLine())
            {
                // "Normal" case: both bounding boxes don't cross the date line
                // Case 1: eastern boundary of other box intersects:
                if ((d->m_east >= other.d->m_east && d->m_west <= other.d->m_east)
                    // Case 2: eastern boundary of this box intersects:
                    || (other.d->m_east >= d->m_east && other.d->m_west <= d->m_east)
                    // Case 3: western boundary of other box intersects:
                    || (d->m_east >= other.d->m_west && d->m_west <= other.d->m_west)
                    // Case 4: western boundary of this box intersects:
                    || (other.d->m_east >= d->m_west && other.d->m_west <= d->m_west))
                {
                    return true;
                }
            }

            else
            {
                // The other bounding box crosses the date line, "this" one does not:
                // So the date line splits the other bounding box in two parts.

                if (d->m_west <= other.d->m_east || d->m_east >= other.d->m_west)
                {
                    return true;
                }
            }
        }

        else
        {
            if (other.crossesDateLine())
            {
                // The trivial case: both bounding boxes cross the date line and intersect
                return true;
            }

            else
            {
                // "This" bounding box crosses the date line, the other one does not.
                // So the date line splits "this" bounding box in two parts.
                //
                // This also covers the case where this bounding box covers the whole
                // longitude range ( -180 <= lon <= + 180 ).
                if (other.d->m_west <= d->m_east || other.d->m_east >= d->m_west)
                {
                    return true;
                }
            }
        }
    }

    return false;
}

GeoDataLatLonBox GeoDataLatLonBox::united(const GeoDataLatLonBox& other) const
{
    if (isEmpty())
    {
        return other;
    }

    if (other.isEmpty())
    {
        return *this;
    }

    GeoDataLatLonBox result;

    // use the position of the centers of the boxes to determine the "smallest"
    // box (i.e. should the total box go through IDL or not). this
    // determination does not depend on one box or the other crossing IDL too
    GeoDataCoordinates c1 = center();
    GeoDataCoordinates c2 = other.center();

    // do latitude first, quite simple
    result.setNorth(qMax(d->m_north, other.north()));
    result.setSouth(qMin(d->m_south, other.south()));

    qreal w1 = d->m_west;
    qreal w2 = other.west();
    qreal e1 = d->m_east;
    qreal e2 = other.east();

    bool const idl1 = d->m_east < d->m_west;
    bool const idl2 = other.d->m_east < other.d->m_west;

    if (idl1)
    {
        w1 += 2 * M_PI;
        e1 += 2 * M_PI;
    }

    if (idl2)
    {
        w2 += 2 * M_PI;
        e2 += 2 * M_PI;
    }

    // in the usual case, we take the maximum of east bounds, and
    // the minimum of west bounds. The exceptions are:
    // - centers of boxes are more than 180 apart
    //    (so the smallest box should go around the IDL)
    //
    // - 1 but not 2 boxes are crossing IDL
    if (fabs(c2.longitude() - c1.longitude()) > M_PI
        || (idl1 ^ idl2))
    {
        // exceptions, we go the unusual way:
        // min of east, max of west
        result.setEast(qMin(e1, e2));
        result.setWest(qMax(w1, w2));
    }

    else
    {
        // normal case, max of east, min of west
        result.setEast(qMax(e1, e2));
        result.setWest(qMin(w1, w2));
    }

    return result;
}

GeoDataLatLonBox GeoDataLatLonBox::toCircumscribedRectangle() const
{
    QVector<GeoDataCoordinates> coordinates;
    coordinates.reserve(4);

    coordinates.append(GeoDataCoordinates(west(), north()));
    coordinates.append(GeoDataCoordinates(west(), south()));
    coordinates.append(GeoDataCoordinates(east() + (crossesDateLine() ? 2 * M_PI : 0), north()));
    coordinates.append(GeoDataCoordinates(east() + (crossesDateLine() ? 2 * M_PI : 0), south()));

    const qreal cosRotation = cos(rotation());
    const qreal sinRotation = sin(rotation());

    qreal centerLat = center().latitude();
    qreal centerLon = center().longitude();

    if (GeoDataLatLonBox(0, 0, center().longitude(), west()).crossesDateLine())
    {
        if (!centerLon)
        {
            centerLon += M_PI;
        }

        else
        {
            centerLon += 2 * M_PI;
        }
    }

    GeoDataLatLonBox box;

    bool northSet = false;
    bool southSet = false;
    bool eastSet = false;
    bool westSet = false;

    for (const GeoDataCoordinates& coord : coordinates)
    {

        const qreal lon = coord.longitude();
        const qreal lat = coord.latitude();

        const qreal rotatedLon = (lon - centerLon) * cosRotation - (lat - centerLat) * sinRotation + centerLon;
        const qreal rotatedLat = (lon - centerLon) * sinRotation + (lat - centerLat) * cosRotation + centerLat;

        if (!northSet || rotatedLat > box.north())
        {
            northSet = true;
            box.setNorth(rotatedLat);
        }

        if (!southSet || rotatedLat < box.south())
        {
            southSet = true;
            box.setSouth(rotatedLat);
        }

        if (!westSet || rotatedLon < box.west())
        {
            westSet = true;
            box.setWest(rotatedLon);
        }

        if (!eastSet || rotatedLon > box.east())
        {
            eastSet = true;
            box.setEast(rotatedLon);
        }
    }

    box.setBoundaries(box.north(), box.south(), box.east(), box.west());

    return box;
}

GeoDataLatLonBox& GeoDataLatLonBox::operator=(const GeoDataLatLonBox& other)
{
    GeoDataObject::operator=(other);

    *d = *other.d;
    return *this;
}

GeoDataLatLonBox GeoDataLatLonBox::operator|(const GeoDataLatLonBox& other) const
{
    return united(other);
}

GeoDataLatLonBox& GeoDataLatLonBox::operator|=(const GeoDataLatLonBox& other)
{
    *this = united(other);
    return *this;
}


void GeoDataLatLonBox::pack(QDataStream& stream) const
{
    GeoDataObject::pack(stream);

    stream << d->m_north << d->m_south << d->m_east << d->m_west << d->m_rotation;
}

void GeoDataLatLonBox::unpack(QDataStream& stream)
{
    GeoDataObject::unpack(stream);

    stream >> d->m_north >> d->m_south >> d->m_east >> d->m_west >> d->m_rotation;
}

GeoDataLatLonBox GeoDataLatLonBox::fromLineString(const GeoDataLineString& lineString)
{
    // If the line string is empty return an empty boundingbox
    if (lineString.isEmpty())
    {
        return GeoDataLatLonBox();
    }

    qreal lon, lat;
    lineString.first().geoCoordinates(lon, lat);
    GeoDataCoordinates::normalizeLonLat(lon, lat);

    qreal north = lat;
    qreal south = lat;
    qreal west =  lon;
    qreal east =  lon;

    // If there's only a single node stored then the boundingbox only contains that point
    if (lineString.size() == 1)
    {
        return GeoDataLatLonBox(north, south, east, west);
    }

    // Specifies whether the polygon crosses the IDL
    bool idlCrossed = false;

    // "idlCrossState" specifies the state concerning IDL crossage.
    // This is needed in order to create optimal bounding boxes in case of covering the IDL
    // Every time the IDL gets crossed from east to west the idlCrossState value gets
    // increased by one.
    // Every time the IDL gets crossed from west to east the idlCrossState value gets
    // decreased by one.

    int idlCrossState = 0;
    int idlMaxCrossState = 0;
    int idlMinCrossState = 0;

    // Holds values for east and west while idlCrossState != 0
    qreal otherWest =  lon;
    qreal otherEast =  lon;

    qreal previousLon = lon;

    int currentSign = (lon < 0) ? -1 : +1;
    int previousSign = currentSign;

    QVector<GeoDataCoordinates>::ConstIterator it(lineString.constBegin());
    QVector<GeoDataCoordinates>::ConstIterator itEnd(lineString.constEnd());

    bool processingLastNode = false;

    while (it != itEnd)
    {
        // Get coordinates and normalize them to the desired range.
        (it)->geoCoordinates(lon, lat);
        GeoDataCoordinates::normalizeLonLat(lon, lat);

        // Determining the maximum and minimum latitude
        if (lat > north)
        {
            north = lat;
        }

        else if (lat < south)
        {
            south = lat;
        }

        currentSign = (lon < 0) ? -1 : +1;

        // Once the polyline crosses the dateline the covered bounding box
        // would cover the whole [-M_PI; M_PI] range.
        // When looking separately at the longitude range that gets covered
        // east and west from the IDL we get two bounding boxes (we prefix
        // the resulting longitude range on the "other side" with "other").
        // By picking the "inner" range values we get a more appropriate
        // optimized single bounding box.

        // IDL check
        if (previousSign != currentSign
            && fabs(previousLon) + fabs(lon) > M_PI)
        {

            // Initialize values for otherWest and otherEast
            if (idlCrossed == false)
            {
                otherWest =  lon;
                otherEast =  lon;
                idlCrossed = true;
            }

            // Determine the new IDL Cross State
            if (previousLon < 0)
            {
                idlCrossState++;

                if (idlCrossState > idlMaxCrossState)
                {
                    idlMaxCrossState = idlCrossState;
                }
            }

            else
            {
                idlCrossState--;

                if (idlCrossState < idlMinCrossState)
                {
                    idlMinCrossState = idlCrossState;
                }
            }
        }

        if (idlCrossState == 0)
        {
            if (lon > east)
            {
                east = lon;
            }

            if (lon < west)
            {
                west = lon;
            }
        }

        else
        {
            if (lon > otherEast)
            {
                otherEast = lon;
            }

            if (lon < otherWest)
            {
                otherWest = lon;
            }
        }

        previousLon = lon;
        previousSign = currentSign;

        if (processingLastNode)
        {
            break;
        }

        ++it;

        if (lineString.isClosed() && it == itEnd)
        {
            it = lineString.constBegin();
            processingLastNode = true;
        }
    }

    if (idlCrossed)
    {
        if (idlMinCrossState < 0)
        {
            east = otherEast;
        }

        if (idlMaxCrossState > 0)
        {
            west = otherWest;
        }

        if ((idlMinCrossState < 0 && idlMaxCrossState > 0)
            || idlMinCrossState < -1  || idlMaxCrossState > 1
            || west <= east)
        {
            east = +M_PI;
            west = -M_PI;

            // if polygon fully in south hemisphere, contain south pole
            if (north < 0)
            {
                south = -M_PI / 2;
            }

            else
            {
                north = M_PI / 2;
            }
        }
    }

    return GeoDataLatLonBox(north, south, east, west);
}

bool GeoDataLatLonBox::isNull() const
{
    return d->m_north == d->m_south && d->m_east == d->m_west;
}

bool GeoDataLatLonBox::isEmpty() const
{
    return *this == empty;
}

bool GeoDataLatLonBox::fuzzyCompare(const GeoDataLatLonBox& lhs,
                                    const GeoDataLatLonBox& rhs,
                                    const qreal factor)
{
    bool equal = true;

    // Check the latitude for approximate equality

    double latDelta = lhs.height() * factor;

    if (fabs(lhs.north() - rhs.north()) > latDelta)
    {
        equal = false;
    }

    if (fabs(lhs.south() - rhs.south()) > latDelta)
    {
        equal = false;
    }


    // Check the longitude for approximate equality

    double lonDelta = lhs.width() * factor;

    double lhsEast = lhs.east();
    double rhsEast = rhs.east();

    if (!GeoDataLatLonBox::crossesDateLine(lhsEast, rhsEast))
    {
        if (fabs(lhsEast - rhsEast) > lonDelta)
        {
            equal = false;
        }
    }

    else
    {
        if (lhsEast < 0 && rhsEast > 0)
        {
            lhsEast += 2 * M_PI;

            if (fabs(lhsEast - rhsEast) > lonDelta)
            {
                equal = false;
            }
        }

        if (lhsEast > 0 && rhsEast < 0)
        {
            rhsEast += 2 * M_PI;

            if (fabs(lhsEast - rhsEast) > lonDelta)
            {
                equal = false;
            }
        }
    }

    double lhsWest = lhs.west();
    double rhsWest = rhs.west();

    if (!GeoDataLatLonBox::crossesDateLine(lhsWest, rhsWest))
    {
        if (fabs(lhsWest - rhsWest) > lonDelta)
        {
            equal = false;
        }
    }

    else
    {
        if (lhsWest < 0 && rhsWest > 0)
        {
            lhsWest += 2 * M_PI;

            if (fabs(lhsWest - rhsWest) > lonDelta)
            {
                equal = false;
            }
        }

        if (lhsWest > 0 && rhsWest < 0)
        {
            rhsWest += 2 * M_PI;

            if (fabs(lhsWest - rhsWest) > lonDelta)
            {
                equal = false;
            }
        }
    }

    return equal;
}


void GeoDataLatLonBox::clear()
{
    *this = empty;
}

} // namespace Marble
